Skip to content

Commit f6d0ea8

Browse files
Michael Hoffmannvivian-hu-zz
Michael Hoffmann
authored andcommitted
docs(cdk-stepper): add guide to create stepper (#14710)
* docs(cdk-stepper): add guide to create stepper * Added guide how to create a custom stepper using the CDK * Added a material example to demonstrate the custom CDK stepper * Made steps from CdkStepper public as it is necessary to build a custom stepper * Review comments
1 parent 3d3179f commit f6d0ea8

12 files changed

+312
-32
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Creating a custom stepper using the CDK stepper
2+
3+
The [CDK stepper](https://material.angular.io/cdk/stepper/overview) allows to build a custom stepper which you can completely style yourself without any specific Material Design styling.
4+
5+
In this guide, we'll learn how we can build our own custom stepper using the CDK stepper. Here is what we'll build by the end of this guide:
6+
7+
<!-- example(cdk-custom-stepper-without-form) -->
8+
9+
## Create our custom stepper component
10+
11+
Now we are ready to create our custom stepper component. Therefore, we need to create a new Angular component which extends `CdkStepper`:
12+
13+
**custom-stepper.component.ts**
14+
15+
```ts
16+
@Component({
17+
selector: "app-custom-stepper",
18+
templateUrl: "./custom-stepper.component.html",
19+
styleUrls: ["./custom-stepper.component.css"],
20+
// This custom stepper provides itself as CdkStepper so that it can be recognized
21+
// by other components.
22+
providers: [{ provide: CdkStepper, useExisting: CustomStepperComponent }]
23+
})
24+
export class CustomStepperComponent extends CdkStepper {
25+
/** Whether the validity of previous steps should be checked or not */
26+
linear: boolean;
27+
28+
/** The index of the selected step. */
29+
selectedIndex: number;
30+
31+
/** The list of step components that the stepper is holding. */
32+
steps: QueryList<CdkStep>;
33+
34+
onClick(index: number): void {
35+
this.selectedIndex = index;
36+
}
37+
}
38+
```
39+
40+
After we've extended our component class from `CdkStepper` we can now access different properties from this class like `linear`, `selectedIndex` and `steps` which are defined in the [API documentation](https://material.angular.io/cdk/stepper/api#CdkStepper).
41+
42+
This is the HTML template of our custom stepper component:
43+
44+
**custom-stepper.component.html**
45+
46+
```html
47+
<section class="container">
48+
<header><h2>Step {{selectedIndex + 1}}/{{steps.length}}</h2></header>
49+
50+
<section *ngFor="let step of steps; let i = index;">
51+
<div [style.display]="selectedIndex === i ? 'block' : 'none'">
52+
<!-- Content from the CdkStep is projected here -->
53+
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
54+
</div>
55+
</section>
56+
57+
<footer class="step-navigation-bar">
58+
<button class="nav-button" cdkStepperPrevious>&larr;</button>
59+
<button
60+
class="step"
61+
*ngFor="let step of steps; let i = index;"
62+
[ngClass]="{'active': selectedIndex === i}"
63+
(click)="onClick(i)"
64+
>
65+
Step {{i + 1}}
66+
</button>
67+
<button class="nav-button" cdkStepperNext>&rarr;</button>
68+
</footer>
69+
</section>
70+
```
71+
72+
In the `app.component.css` file we can now style the stepper however we want:
73+
74+
**custom-stepper.component.css**
75+
76+
```css
77+
.example-container {
78+
border: 1px solid black;
79+
padding: 10px;
80+
margin: 10px;
81+
}
82+
83+
.example-step-navigation-bar {
84+
display: flex;
85+
justify-content: flex-start;
86+
margin-top: 10px;
87+
}
88+
89+
.example-active {
90+
color: blue;
91+
}
92+
93+
.example-step {
94+
background: transparent;
95+
border: 0;
96+
margin: 0 10px;
97+
padding: 10px;
98+
color: black;
99+
}
100+
101+
.example-step.example-active {
102+
color: blue;
103+
border-bottom: 1px solid blue;
104+
}
105+
106+
.example-nav-button {
107+
background: transparent;
108+
border: 0;
109+
}
110+
```
111+
112+
## Using our new custom stepper component
113+
114+
Now we are ready to use our new custom stepper component and fill it with steps. Therefore we can, for example, add it to our `app.component.html` and define some steps:
115+
116+
**app.component.html**
117+
118+
```html
119+
<app-custom-stepper>
120+
<cdk-step><p>This is any content of "Step 1"</p></cdk-step>
121+
<cdk-step><p>This is any content of "Step 2"</p></cdk-step>
122+
</app-custom-stepper>
123+
```
124+
125+
As you can see in this example, each step needs to be wrapped inside a `<cdk-step>` tag.
126+
127+
If you want to iterate over your steps and use your own custom component you can do it, for example, this way:
128+
129+
```html
130+
<app-custom-stepper>
131+
<cdk-step *ngFor="let step of mySteps; let stepIndex = index">
132+
<my-step-component [step]="step"></my-step-component>
133+
</cdk-step>
134+
</app-custom-stepper>
135+
```
136+
137+
## Linear mode
138+
139+
The above example allows the user to freely navigate between all steps. The `CdkStepper` additionally provides the linear mode which requires the user to complete previous steps before proceeding.
140+
141+
A simple example without using forms could look this way:
142+
143+
**app.component.html**
144+
145+
```html
146+
<app-custom-stepper linear>
147+
<cdk-step editable="false" [completed]="completed">
148+
<input type="text" name="a" value="Cannot proceed to next step" />
149+
<button (click)="completeStep()">Complete step</button>
150+
</cdk-step>
151+
<cdk-step editable="false">
152+
<input type="text" name="b" value="b" />
153+
</cdk-step>
154+
</app-custom-stepper>
155+
```
156+
157+
**app.component.ts**
158+
159+
```ts
160+
export class AppComponent {
161+
completed = false;
162+
163+
completeStep(): void {
164+
this.completed = true;
165+
}
166+
}
167+
```

src/cdk/stepper/stepper.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,18 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
247247
*/
248248
private _document: Document | undefined;
249249

250-
/** The list of step components that the stepper is holding. */
250+
/**
251+
* The list of step components that the stepper is holding.
252+
* @deprecated use `steps` instead
253+
* @breaking-change 9.0.0 remove this property
254+
*/
251255
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;
252256

257+
/** The list of step components that the stepper is holding. */
258+
get steps(): QueryList<CdkStep> {
259+
return this._steps;
260+
}
261+
253262
/**
254263
* The list of step headers of the steps in the stepper.
255264
* @deprecated Type to be changed to `QueryList<CdkStepHeader>`.
@@ -267,15 +276,15 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
267276
@Input()
268277
get selectedIndex() { return this._selectedIndex; }
269278
set selectedIndex(index: number) {
270-
if (this._steps) {
279+
if (this.steps) {
271280
// Ensure that the index can't be out of bounds.
272-
if (index < 0 || index > this._steps.length - 1) {
281+
if (index < 0 || index > this.steps.length - 1) {
273282
throw Error('cdkStepper: Cannot assign out-of-bounds value to `selectedIndex`.');
274283
}
275284

276285
if (this._selectedIndex != index &&
277286
!this._anyControlsInvalidOrPending(index) &&
278-
(index >= this._selectedIndex || this._steps.toArray()[index].editable)) {
287+
(index >= this._selectedIndex || this.steps.toArray()[index].editable)) {
279288
this._updateSelectedItemIndex(index);
280289
}
281290
} else {
@@ -288,10 +297,10 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
288297
@Input()
289298
get selected(): CdkStep {
290299
// @breaking-change 8.0.0 Change return type to `CdkStep | undefined`.
291-
return this._steps ? this._steps.toArray()[this.selectedIndex] : undefined!;
300+
return this.steps ? this.steps.toArray()[this.selectedIndex] : undefined!;
292301
}
293302
set selected(step: CdkStep) {
294-
this.selectedIndex = this._steps ? this._steps.toArray().indexOf(step) : -1;
303+
this.selectedIndex = this.steps ? this.steps.toArray().indexOf(step) : -1;
295304
}
296305

297306
/** Event emitted when the selected step has changed. */
@@ -327,7 +336,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
327336

328337
this._keyManager.updateActiveItemIndex(this._selectedIndex);
329338

330-
this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
339+
this.steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
331340
if (!this.selected) {
332341
this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
333342
}
@@ -341,7 +350,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
341350

342351
/** Selects and focuses the next step in list. */
343352
next(): void {
344-
this.selectedIndex = Math.min(this._selectedIndex + 1, this._steps.length - 1);
353+
this.selectedIndex = Math.min(this._selectedIndex + 1, this.steps.length - 1);
345354
}
346355

347356
/** Selects and focuses the previous step in list. */
@@ -352,7 +361,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
352361
/** Resets the stepper to its initial state. Note that this includes clearing form data. */
353362
reset(): void {
354363
this._updateSelectedItemIndex(0);
355-
this._steps.forEach(step => step.reset());
364+
this.steps.forEach(step => step.reset());
356365
this._stateChanged();
357366
}
358367

@@ -384,7 +393,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
384393

385394
/** Returns the type of icon to be displayed. */
386395
_getIndicatorType(index: number, state: StepState = STEP_STATE.NUMBER): StepState {
387-
const step = this._steps.toArray()[index];
396+
const step = this.steps.toArray()[index];
388397
const isCurrentStep = this._isCurrentStep(index);
389398

390399
return step._displayDefaultIndicatorType
@@ -429,7 +438,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
429438
}
430439

431440
private _updateSelectedItemIndex(newIndex: number): void {
432-
const stepsArray = this._steps.toArray();
441+
const stepsArray = this.steps.toArray();
433442
this.selectionChange.emit({
434443
selectedIndex: newIndex,
435444
previouslySelectedIndex: this._selectedIndex,
@@ -469,7 +478,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
469478
}
470479

471480
private _anyControlsInvalidOrPending(index: number): boolean {
472-
const steps = this._steps.toArray();
481+
const steps = this.steps.toArray();
473482

474483
steps[this._selectedIndex].interacted = true;
475484

src/lib/stepper/stepper-horizontal.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<div class="mat-horizontal-stepper-header-container">
2-
<ng-container *ngFor="let step of _steps; let i = index; let isLast = last">
2+
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
33
<mat-step-header class="mat-horizontal-stepper-header"
44
(click)="step.select()"
55
(keydown)="_onKeydown($event)"
66
[tabIndex]="_getFocusIndex() === i ? 0 : -1"
77
[id]="_getStepLabelId(i)"
88
[attr.aria-posinset]="i + 1"
9-
[attr.aria-setsize]="_steps.length"
9+
[attr.aria-setsize]="steps.length"
1010
[attr.aria-controls]="_getStepContentId(i)"
1111
[attr.aria-selected]="selectedIndex == i"
1212
[attr.aria-label]="step.ariaLabel || null"
@@ -25,7 +25,7 @@
2525
</div>
2626

2727
<div class="mat-horizontal-content-container">
28-
<div *ngFor="let step of _steps; let i = index"
28+
<div *ngFor="let step of steps; let i = index"
2929
class="mat-horizontal-stepper-content" role="tabpanel"
3030
[@stepTransition]="_getAnimationDirection(i)"
3131
(@stepTransition.done)="_animationDone.next($event)"

src/lib/stepper/stepper-vertical.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
<div class="mat-step" *ngFor="let step of _steps; let i = index; let isLast = last">
1+
<div class="mat-step" *ngFor="let step of steps; let i = index; let isLast = last">
22
<mat-step-header class="mat-vertical-stepper-header"
33
(click)="step.select()"
44
(keydown)="_onKeydown($event)"
55
[tabIndex]="_getFocusIndex() == i ? 0 : -1"
66
[id]="_getStepLabelId(i)"
77
[attr.aria-posinset]="i + 1"
8-
[attr.aria-setsize]="_steps.length"
8+
[attr.aria-setsize]="steps.length"
99
[attr.aria-controls]="_getStepContentId(i)"
1010
[attr.aria-selected]="selectedIndex === i"
1111
[attr.aria-label]="step.ariaLabel || null"

0 commit comments

Comments
 (0)