Skip to content

docs(cdk-stepper): add guide to create stepper #14710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 16, 2019
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
167 changes: 167 additions & 0 deletions guides/creating-a-custom-stepper-using-the-cdk-stepper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Creating a custom stepper using the CDK stepper

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.

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:

<!-- example(cdk-custom-stepper-without-form) -->

## Create our custom stepper component

Now we are ready to create our custom stepper component. Therefore, we need to create a new Angular component which extends `CdkStepper`:

**custom-stepper.component.ts**

```ts
@Component({
selector: "app-custom-stepper",
templateUrl: "./custom-stepper.component.html",
styleUrls: ["./custom-stepper.component.css"],
// This custom stepper provides itself as CdkStepper so that it can be recognized
// by other components.
providers: [{ provide: CdkStepper, useExisting: CustomStepperComponent }]
})
export class CustomStepperComponent extends CdkStepper {
/** Whether the validity of previous steps should be checked or not */
linear: boolean;

/** The index of the selected step. */
selectedIndex: number;

/** The list of step components that the stepper is holding. */
steps: QueryList<CdkStep>;

onClick(index: number): void {
this.selectedIndex = index;
}
}
```

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).

This is the HTML template of our custom stepper component:

**custom-stepper.component.html**

```html
<section class="container">
<header><h2>Step {{selectedIndex + 1}}/{{steps.length}}</h2></header>

<section *ngFor="let step of steps; let i = index;">
<div [style.display]="selectedIndex === i ? 'block' : 'none'">
<!-- Content from the CdkStep is projected here -->
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
</div>
</section>

<footer class="step-navigation-bar">
<button class="nav-button" cdkStepperPrevious>&larr;</button>
<button
class="step"
*ngFor="let step of steps; let i = index;"
[ngClass]="{'active': selectedIndex === i}"
(click)="onClick(i)"
>
Step {{i + 1}}
</button>
<button class="nav-button" cdkStepperNext>&rarr;</button>
</footer>
</section>
```

In the `app.component.css` file we can now style the stepper however we want:

**custom-stepper.component.css**

```css
.example-container {
border: 1px solid black;
padding: 10px;
margin: 10px;
}

.example-step-navigation-bar {
display: flex;
justify-content: flex-start;
margin-top: 10px;
}

.example-active {
color: blue;
}

.example-step {
background: transparent;
border: 0;
margin: 0 10px;
padding: 10px;
color: black;
}

.example-step.example-active {
color: blue;
border-bottom: 1px solid blue;
}

.example-nav-button {
background: transparent;
border: 0;
}
```

## Using our new custom stepper component

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:

**app.component.html**

```html
<app-custom-stepper>
<cdk-step><p>This is any content of "Step 1"</p></cdk-step>
<cdk-step><p>This is any content of "Step 2"</p></cdk-step>
</app-custom-stepper>
```

As you can see in this example, each step needs to be wrapped inside a `<cdk-step>` tag.

If you want to iterate over your steps and use your own custom component you can do it, for example, this way:

```html
<app-custom-stepper>
<cdk-step *ngFor="let step of mySteps; let stepIndex = index">
<my-step-component [step]="step"></my-step-component>
</cdk-step>
</app-custom-stepper>
```

## Linear mode

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.

A simple example without using forms could look this way:

**app.component.html**

```html
<app-custom-stepper linear>
<cdk-step editable="false" [completed]="completed">
<input type="text" name="a" value="Cannot proceed to next step" />
<button (click)="completeStep()">Complete step</button>
</cdk-step>
<cdk-step editable="false">
<input type="text" name="b" value="b" />
</cdk-step>
</app-custom-stepper>
```

**app.component.ts**

```ts
export class AppComponent {
completed = false;

completeStep(): void {
this.completed = true;
}
}
```
33 changes: 21 additions & 12 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,18 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
*/
private _document: Document | undefined;

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

/** The list of step components that the stepper is holding. */
get steps(): QueryList<CdkStep> {
return this._steps;
}

/**
* The list of step headers of the steps in the stepper.
* @deprecated Type to be changed to `QueryList<CdkStepHeader>`.
Expand All @@ -267,15 +276,15 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
@Input()
get selectedIndex() { return this._selectedIndex; }
set selectedIndex(index: number) {
if (this._steps) {
if (this.steps) {
// Ensure that the index can't be out of bounds.
if (index < 0 || index > this._steps.length - 1) {
if (index < 0 || index > this.steps.length - 1) {
throw Error('cdkStepper: Cannot assign out-of-bounds value to `selectedIndex`.');
}

if (this._selectedIndex != index &&
!this._anyControlsInvalidOrPending(index) &&
(index >= this._selectedIndex || this._steps.toArray()[index].editable)) {
(index >= this._selectedIndex || this.steps.toArray()[index].editable)) {
this._updateSelectedItemIndex(index);
}
} else {
Expand All @@ -288,10 +297,10 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
@Input()
get selected(): CdkStep {
// @breaking-change 8.0.0 Change return type to `CdkStep | undefined`.
return this._steps ? this._steps.toArray()[this.selectedIndex] : undefined!;
return this.steps ? this.steps.toArray()[this.selectedIndex] : undefined!;
}
set selected(step: CdkStep) {
this.selectedIndex = this._steps ? this._steps.toArray().indexOf(step) : -1;
this.selectedIndex = this.steps ? this.steps.toArray().indexOf(step) : -1;
}

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

this._keyManager.updateActiveItemIndex(this._selectedIndex);

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

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

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

Expand Down Expand Up @@ -384,7 +393,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {

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

return step._displayDefaultIndicatorType
Expand Down Expand Up @@ -429,7 +438,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
}

private _updateSelectedItemIndex(newIndex: number): void {
const stepsArray = this._steps.toArray();
const stepsArray = this.steps.toArray();
this.selectionChange.emit({
selectedIndex: newIndex,
previouslySelectedIndex: this._selectedIndex,
Expand Down Expand Up @@ -469,7 +478,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
}

private _anyControlsInvalidOrPending(index: number): boolean {
const steps = this._steps.toArray();
const steps = this.steps.toArray();

steps[this._selectedIndex].interacted = true;

Expand Down
6 changes: 3 additions & 3 deletions src/lib/stepper/stepper-horizontal.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div class="mat-horizontal-stepper-header-container">
<ng-container *ngFor="let step of _steps; let i = index; let isLast = last">
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
<mat-step-header class="mat-horizontal-stepper-header"
(click)="step.select()"
(keydown)="_onKeydown($event)"
[tabIndex]="_getFocusIndex() === i ? 0 : -1"
[id]="_getStepLabelId(i)"
[attr.aria-posinset]="i + 1"
[attr.aria-setsize]="_steps.length"
[attr.aria-setsize]="steps.length"
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[attr.aria-label]="step.ariaLabel || null"
Expand All @@ -25,7 +25,7 @@
</div>

<div class="mat-horizontal-content-container">
<div *ngFor="let step of _steps; let i = index"
<div *ngFor="let step of steps; let i = index"
class="mat-horizontal-stepper-content" role="tabpanel"
[@stepTransition]="_getAnimationDirection(i)"
(@stepTransition.done)="_animationDone.next($event)"
Expand Down
4 changes: 2 additions & 2 deletions src/lib/stepper/stepper-vertical.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<div class="mat-step" *ngFor="let step of _steps; let i = index; let isLast = last">
<div class="mat-step" *ngFor="let step of steps; let i = index; let isLast = last">
<mat-step-header class="mat-vertical-stepper-header"
(click)="step.select()"
(keydown)="_onKeydown($event)"
[tabIndex]="_getFocusIndex() == i ? 0 : -1"
[id]="_getStepLabelId(i)"
[attr.aria-posinset]="i + 1"
[attr.aria-setsize]="_steps.length"
[attr.aria-setsize]="steps.length"
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex === i"
[attr.aria-label]="step.ariaLabel || null"
Expand Down
Loading