diff --git a/guides/creating-a-custom-stepper-using-the-cdk-stepper.md b/guides/creating-a-custom-stepper-using-the-cdk-stepper.md new file mode 100644 index 000000000000..11b55934f6fb --- /dev/null +++ b/guides/creating-a-custom-stepper-using-the-cdk-stepper.md @@ -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: + + + +## 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; + + 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 +
+

Step {{selectedIndex + 1}}/{{steps.length}}

+ +
+
+ + +
+
+ +
+ + + +
+
+``` + +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 + +

This is any content of "Step 1"

+

This is any content of "Step 2"

+
+``` + +As you can see in this example, each step needs to be wrapped inside a `` tag. + +If you want to iterate over your steps and use your own custom component you can do it, for example, this way: + +```html + + + + + +``` + +## 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.component.ts** + +```ts +export class AppComponent { + completed = false; + + completeStep(): void { + this.completed = true; + } +} +``` diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 929a8fe83ec2..3545daf1648c 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -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; + /** The list of step components that the stepper is holding. */ + get steps(): QueryList { + return this._steps; + } + /** * The list of step headers of the steps in the stepper. * @deprecated Type to be changed to `QueryList`. @@ -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 { @@ -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. */ @@ -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); } @@ -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. */ @@ -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(); } @@ -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 @@ -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, @@ -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; diff --git a/src/lib/stepper/stepper-horizontal.html b/src/lib/stepper/stepper-horizontal.html index 991d96f05b84..0eb54b01a86e 100644 --- a/src/lib/stepper/stepper-horizontal.html +++ b/src/lib/stepper/stepper-horizontal.html @@ -1,12 +1,12 @@
- +
-
+
{ let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance; stepperComponent.selectedIndex = 1; - stepperComponent._steps.toArray()[0].editable = false; + stepperComponent.steps.toArray()[0].editable = false; let previousButtonNativeEl = fixture.debugElement .queryAll(By.directive(MatStepperPrevious))[1].nativeElement; previousButtonNativeEl.click(); @@ -267,7 +272,7 @@ describe('MatStepper', () => { expect(stepperComponent.selectedIndex).toBe(1); - stepperComponent._steps.toArray()[0].editable = true; + stepperComponent.steps.toArray()[0].editable = true; previousButtonNativeEl.click(); fixture.detectChanges(); @@ -279,7 +284,7 @@ describe('MatStepper', () => { let nextButtonNativeEl = fixture.debugElement .queryAll(By.directive(MatStepperNext))[0].nativeElement; expect(stepperComponent._getIndicatorType(0)).toBe('number'); - stepperComponent._steps.toArray()[0].editable = true; + stepperComponent.steps.toArray()[0].editable = true; nextButtonNativeEl.click(); fixture.detectChanges(); @@ -291,7 +296,7 @@ describe('MatStepper', () => { let nextButtonNativeEl = fixture.debugElement .queryAll(By.directive(MatStepperNext))[0].nativeElement; expect(stepperComponent._getIndicatorType(0)).toBe('number'); - stepperComponent._steps.toArray()[0].editable = false; + stepperComponent.steps.toArray()[0].editable = false; nextButtonNativeEl.click(); fixture.detectChanges(); @@ -415,7 +420,7 @@ describe('MatStepper', () => { const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper)); const stepperComponent: MatStepper = stepperDebugElement.componentInstance; - stepperComponent._steps.toArray()[0].editable = true; + stepperComponent.steps.toArray()[0].editable = true; stepperComponent.next(); fixture.detectChanges(); @@ -428,7 +433,7 @@ describe('MatStepper', () => { const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper)); const stepperComponent: MatStepper = stepperDebugElement.componentInstance; - stepperComponent._steps.toArray()[0].editable = false; + stepperComponent.steps.toArray()[0].editable = false; stepperComponent.next(); fixture.detectChanges(); @@ -591,7 +596,7 @@ describe('MatStepper', () => { stepperComponent.selectedIndex = 2; fixture.detectChanges(); - expect(stepperComponent._steps.toArray()[2].optional).toBe(true); + expect(stepperComponent.steps.toArray()[2].optional).toBe(true); expect(stepperComponent.selectedIndex).toBe(2); expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true); @@ -614,7 +619,7 @@ describe('MatStepper', () => { }); it('should be able to reset the stepper to its initial state', () => { - const steps = stepperComponent._steps.toArray(); + const steps = stepperComponent.steps.toArray(); testComponent.oneGroup.get('oneCtrl')!.setValue('value'); fixture.detectChanges(); @@ -650,7 +655,7 @@ describe('MatStepper', () => { }); it('should reset back to the first step when some of the steps are not editable', () => { - const steps = stepperComponent._steps.toArray(); + const steps = stepperComponent.steps.toArray(); steps[0].editable = false; @@ -669,7 +674,7 @@ describe('MatStepper', () => { }); it('should not clobber the `complete` binding when resetting', () => { - const steps: MatStep[] = stepperComponent._steps.toArray(); + const steps: CdkStep[] = stepperComponent.steps.toArray(); const fillOutStepper = () => { testComponent.oneGroup.get('oneCtrl')!.setValue('input'); testComponent.twoGroup.get('twoCtrl')!.setValue('input'); @@ -933,7 +938,7 @@ describe('MatStepper', () => { .queryAll(By.directive(MatStepperNext))[0].nativeElement; stepper.selectedIndex = 1; - stepper._steps.first.hasError = true; + stepper.steps.first.hasError = true; nextButtonNativeEl.click(); fixture.detectChanges(); @@ -950,7 +955,7 @@ describe('MatStepper', () => { expect(stepper._getIndicatorType(0)).toBe(STEP_STATE.ERROR); - stepper._steps.first.hasError = false; + stepper.steps.first.hasError = false; fixture.detectChanges(); expect(stepper._getIndicatorType(0)).not.toBe(STEP_STATE.ERROR); @@ -981,7 +986,7 @@ describe('MatStepper', () => { .queryAll(By.directive(MatStepperNext))[0].nativeElement; stepper.selectedIndex = 1; - stepper._steps.first.completed = true; + stepper.steps.first.completed = true; nextButtonNativeEl.click(); fixture.detectChanges(); @@ -990,7 +995,7 @@ describe('MatStepper', () => { it('should show edit state when step is editable and its the current step', () => { stepper.selectedIndex = 1; - stepper._steps.toArray()[1].editable = true; + stepper.steps.toArray()[1].editable = true; fixture.detectChanges(); expect(stepper._getIndicatorType(1)).toBe(STEP_STATE.EDIT); diff --git a/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.css b/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.html b/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.html new file mode 100644 index 000000000000..05e2b1dd3e70 --- /dev/null +++ b/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.html @@ -0,0 +1,8 @@ + + +

This is any content of "Step 1"

+
+ +

This is any content of "Step 2"

+
+
\ No newline at end of file diff --git a/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.ts b/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.ts new file mode 100644 index 000000000000..099c6b289e41 --- /dev/null +++ b/src/material-examples/cdk-custom-stepper-without-form/cdk-custom-stepper-without-form-example.ts @@ -0,0 +1,37 @@ +import {Component, QueryList, ChangeDetectorRef} from '@angular/core'; +import {CdkStepper, CdkStep} from '@angular/cdk/stepper'; +import {Directionality} from '@angular/cdk/bidi'; + +/** @title A custom CDK stepper without a form */ +@Component({ + selector: 'cdk-custom-stepper-without-form-example', + templateUrl: './cdk-custom-stepper-without-form-example.html', + styleUrls: ['./cdk-custom-stepper-without-form-example.css'] +}) +export class CdkCustomStepperWithoutFormExample {} + +/** Custom CDK stepper component */ +@Component({ + selector: 'example-custom-stepper', + templateUrl: './example-custom-stepper.html', + styleUrls: ['./example-custom-stepper.css'], + providers: [{ provide: CdkStepper, useExisting: CustomStepper }], +}) +export class CustomStepper 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; + + constructor(dir: Directionality, changeDetectorRef: ChangeDetectorRef) { + super(dir, changeDetectorRef); + } + + onClick(index: number): void { + this.selectedIndex = index; + } +} diff --git a/src/material-examples/cdk-custom-stepper-without-form/example-custom-stepper.css b/src/material-examples/cdk-custom-stepper-without-form/example-custom-stepper.css new file mode 100644 index 000000000000..3a453707f996 --- /dev/null +++ b/src/material-examples/cdk-custom-stepper-without-form/example-custom-stepper.css @@ -0,0 +1,33 @@ +.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; +} diff --git a/src/material-examples/cdk-custom-stepper-without-form/example-custom-stepper.html b/src/material-examples/cdk-custom-stepper-without-form/example-custom-stepper.html new file mode 100644 index 000000000000..411bdee41dae --- /dev/null +++ b/src/material-examples/cdk-custom-stepper-without-form/example-custom-stepper.html @@ -0,0 +1,17 @@ +
+
+

Step {{selectedIndex + 1}}/{{steps.length}}

+
+ +
+
+ +
+
+ +
+ + + +
+
\ No newline at end of file diff --git a/src/material-examples/material-module.ts b/src/material-examples/material-module.ts index 956f231eda77..2b1fc6580c9b 100644 --- a/src/material-examples/material-module.ts +++ b/src/material-examples/material-module.ts @@ -5,6 +5,7 @@ import {A11yModule} from '@angular/cdk/a11y'; import {CdkTableModule} from '@angular/cdk/table'; import {CdkTreeModule} from '@angular/cdk/tree'; import {DragDropModule} from '@angular/cdk/drag-drop'; +import {CdkStepperModule} from '@angular/cdk/stepper'; import {PortalModule} from '@angular/cdk/portal'; import { MatAutocompleteModule, MatBadgeModule, MatBottomSheetModule, MatButtonModule, @@ -21,6 +22,7 @@ import { A11yModule, CdkTableModule, CdkTreeModule, + CdkStepperModule, DragDropModule, MatAutocompleteModule, MatBadgeModule, @@ -64,6 +66,7 @@ import { A11yModule, CdkTableModule, CdkTreeModule, + CdkStepperModule, DragDropModule, MatAutocompleteModule, MatBadgeModule, diff --git a/tools/public_api_guard/cdk/stepper.d.ts b/tools/public_api_guard/cdk/stepper.d.ts index 311ee234cd33..7e01551690e7 100644 --- a/tools/public_api_guard/cdk/stepper.d.ts +++ b/tools/public_api_guard/cdk/stepper.d.ts @@ -41,6 +41,7 @@ export declare class CdkStepper implements AfterViewInit, OnDestroy { selected: CdkStep; selectedIndex: number; selectionChange: EventEmitter; + readonly steps: QueryList; constructor(_dir: Directionality, _changeDetectorRef: ChangeDetectorRef, _elementRef?: ElementRef | undefined, _document?: any); _getAnimationDirection(index: number): StepContentPositionState; _getFocusIndex(): number | null;