Skip to content

Commit

Permalink
feat(stepper): allow for header icons to be customized
Browse files Browse the repository at this point in the history
Currently users are locked into using the Material `create` and `done` icon for the step headers. These changes add the ability to customize the icons by providing an `ng-template` with an override.

Fixes angular#7384.
  • Loading branch information
crisbeto committed Nov 13, 2017
1 parent 8dfe470 commit f120014
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/lib/stepper/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './stepper';
export * from './stepper-button';
export * from './step-header';
export * from './stepper-intl';
export * from './stepper-icon';
19 changes: 14 additions & 5 deletions src/lib/stepper/step-header.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
<div class="mat-step-header-ripple" mat-ripple [matRippleTrigger]="_getHostElement()"></div>
<div [class.mat-step-icon]="icon !== 'number' || selected"
[class.mat-step-icon-not-touched]="icon == 'number' && !selected"
[ngSwitch]="icon">
<div [class.mat-step-icon]="state !== 'number' || selected"
[class.mat-step-icon-not-touched]="state == 'number' && !selected"
[ngSwitch]="state">

<span *ngSwitchCase="'number'">{{index + 1}}</span>
<mat-icon *ngSwitchCase="'edit'">create</mat-icon>
<mat-icon *ngSwitchCase="'done'">done</mat-icon>

<ng-container *ngSwitchCase="'edit'" [ngSwitch]="!!(iconOverrides && iconOverrides.edit)">
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.edit"></ng-container>
<mat-icon *ngSwitchDefault>create</mat-icon>
</ng-container>

<ng-container *ngSwitchCase="'done'" [ngSwitch]="!!(iconOverrides && iconOverrides.done)">
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.done"></ng-container>
<mat-icon *ngSwitchDefault>done</mat-icon>
</ng-container>
</div>
<div class="mat-step-label"
[class.mat-step-label-active]="active"
Expand Down
8 changes: 6 additions & 2 deletions src/lib/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Input,
OnDestroy,
ViewEncapsulation,
TemplateRef,
} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {MatStepLabel} from './step-label';
Expand All @@ -38,12 +39,15 @@ import {MatStepperIntl} from './stepper-intl';
export class MatStepHeader implements OnDestroy {
private _intlSubscription: Subscription;

/** Icon for the given step. */
@Input() icon: string;
/** State of the given step. */
@Input() state: string;

/** Label of the given step. */
@Input() label: MatStepLabel | string;

/** Overrides for the header icons, passed in via the stepper. */
@Input() iconOverrides: {[key: string]: TemplateRef<any>};

/** Index of the given step. */
@Input()
get index() { return this._index; }
Expand Down
5 changes: 3 additions & 2 deletions src/lib/stepper/stepper-horizontal.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[index]="i"
[icon]="_getIndicatorType(i)"
[state]="_getIndicatorType(i)"
[label]="step.stepLabel || step.label"
[selected]="selectedIndex === i"
[active]="step.completed || selectedIndex === i || !linear"
[optional]="step.optional">
[optional]="step.optional"
[iconOverrides]="_iconOverrides">
</mat-step-header>
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
</ng-container>
Expand Down
22 changes: 22 additions & 0 deletions src/lib/stepper/stepper-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Input, TemplateRef} from '@angular/core';

/**
* Template to be used to override the icons inside the step header.
*/
@Directive({
selector: 'ng-template[matStepperIcon]',
})
export class MatStepperIcon {
/** Name of the icon to be overridden. */
@Input('matStepperIcon') name: 'edit' | 'done';

constructor(public templateRef: TemplateRef<any>) { }
}
19 changes: 15 additions & 4 deletions src/lib/stepper/stepper-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {MatCommonModule, MatRippleModule, ErrorStateMatcher} from '@angular/mate
import {MatIconModule} from '@angular/material/icon';
import {MatStepHeader} from './step-header';
import {MatStepLabel} from './step-label';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
import {MatStepperIntl} from './stepper-intl';
import {MatStepperIcon} from './stepper-icon';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';


@NgModule({
Expand All @@ -41,10 +42,20 @@ import {MatStepperIntl} from './stepper-intl';
MatStepper,
MatStepperNext,
MatStepperPrevious,
MatStepHeader
MatStepHeader,
MatStepperIcon,
],
declarations: [
MatHorizontalStepper,
MatVerticalStepper,
MatStep,
MatStepLabel,
MatStepper,
MatStepperNext,
MatStepperPrevious,
MatStepHeader,
MatStepperIcon,
],
declarations: [MatHorizontalStepper, MatVerticalStepper, MatStep, MatStepLabel, MatStepper,
MatStepperNext, MatStepperPrevious, MatStepHeader],
providers: [MatStepperIntl, ErrorStateMatcher],
})
export class MatStepperModule {}
5 changes: 3 additions & 2 deletions src/lib/stepper/stepper-vertical.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex === i"
[index]="i"
[icon]="_getIndicatorType(i)"
[state]="_getIndicatorType(i)"
[label]="step.stepLabel || step.label"
[selected]="selectedIndex === i"
[active]="step.completed || selectedIndex === i || !linear"
[optional]="step.optional">
[optional]="step.optional"
[iconOverrides]="_iconOverrides">
</mat-step-header>

<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,25 @@ By default, the `completed` attribute of a step returns `true` if the step is va
linear stepper) and the user has interacted with the step. The user, however, can also override
this default `completed` behavior by setting the `completed` attribute as needed.

#### Overriding icons
By default, the step headers will use the `create` and `done` icons from the Material design icon
set via `<mat-icon>` elements. If you want to provide a different set of icons, you can do so
by placing a `matStepperIcon` for each of the icons that you want to override:

```html
<mat-vertical-stepper>
<ng-template matStepperIcon="edit">
<custom-icon>edit</custom-icon>
</ng-template>

<ng-template matStepperIcon="done">
<custom-icon>done</custom-icon>
</ng-template>

<!-- Stepper steps go here -->
</mat-vertical-stepper>
```

### Keyboard interaction
- <kbd>LEFT_ARROW</kbd>: Focuses the previous step header
- <kbd>RIGHT_ARROW</kbd>: Focuses the next step header
Expand Down
53 changes: 52 additions & 1 deletion src/lib/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ describe('MatHorizontalStepper', () => {
declarations: [
SimpleMatHorizontalStepperApp,
SimplePreselectedMatHorizontalStepperApp,
LinearMatHorizontalStepperApp
LinearMatHorizontalStepperApp,
IconOverridesStepper,
],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})}
Expand Down Expand Up @@ -133,6 +134,41 @@ describe('MatHorizontalStepper', () => {
});
});

describe('icon overrides', () => {
let fixture: ComponentFixture<IconOverridesStepper>;

beforeEach(() => {
fixture = TestBed.createComponent(IconOverridesStepper);
fixture.detectChanges();
});

it('should allow for the `edit` icon to be overridden', () => {
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;

stepperComponent._steps.toArray()[0].editable = true;
stepperComponent.next();
fixture.detectChanges();

const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');

expect(header.textContent).toContain('Custom edit');
});

it('should allow for the `done` icon to be overridden', () => {
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;

stepperComponent._steps.toArray()[0].editable = false;
stepperComponent.next();
fixture.detectChanges();

const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');

expect(header.textContent).toContain('Custom done');
});
});

describe('linear horizontal stepper', () => {
let fixture: ComponentFixture<LinearMatHorizontalStepperApp>;
let testComponent: LinearMatHorizontalStepperApp;
Expand Down Expand Up @@ -888,3 +924,18 @@ class LinearMatVerticalStepperApp {
class SimplePreselectedMatHorizontalStepperApp {
index = 0;
}

@Component({
template: `
<mat-horizontal-stepper>
<ng-template matStepperIcon="edit">Custom edit</ng-template>
<ng-template matStepperIcon="done">Custom done</ng-template>
<mat-step>Content 1</mat-step>
<mat-step>Content 2</mat-step>
<mat-step>Content 3</mat-step>
</mat-horizontal-stepper>
`
})
class IconOverridesStepper {}

26 changes: 25 additions & 1 deletion src/lib/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ import {
ViewChildren,
ViewEncapsulation,
ChangeDetectionStrategy,
TemplateRef,
AfterContentInit,
} from '@angular/core';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
import {ErrorStateMatcher} from '@angular/material/core';
import {MatStepHeader} from './step-header';
import {MatStepLabel} from './step-label';
import {MatStepperIcon} from './stepper-icon';

/** Workaround for https://github.com/angular/angular/issues/17849 */
export const _MatStep = CdkStep;
Expand Down Expand Up @@ -63,15 +66,36 @@ export class MatStep extends _MatStep implements ErrorStateMatcher {
}
}


@Directive({
selector: '[matStepper]'
})
export class MatStepper extends _MatStepper {
export class MatStepper extends _MatStepper implements AfterContentInit {
/** The list of step headers of the steps in the stepper. */
@ViewChildren(MatStepHeader, {read: ElementRef}) _stepHeader: QueryList<ElementRef>;

/** Steps that the stepper holds. */
@ContentChildren(MatStep) _steps: QueryList<MatStep>;

/** Custom icon overrides passed in by the consumer. */
@ContentChildren(MatStepperIcon) _icons: QueryList<MatStepperIcon>;

/** Consumer-specified template-refs to be used to override the header icons. */
_iconOverrides: {[key: string]: TemplateRef<any>} = {};

ngAfterContentInit() {
const icons = this._icons.toArray();
const editOverride = icons.find(icon => icon.name === 'edit');
const doneOverride = icons.find(icon => icon.name === 'done');

if (editOverride) {
this._iconOverrides.edit = editOverride.templateRef;
}

if (doneOverride) {
this._iconOverrides.done = doneOverride.templateRef;
}
}
}

@Component({
Expand Down

5 comments on commit f120014

@agrawalharsh
Copy link

@agrawalharsh agrawalharsh commented on f120014 Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried using in this format
<ng-template matStepperIcon="edit"> <custom-icon>edit</custom-icon> </ng-template>
but it is giving me error 'custom-icon' is not a known element: , I know I am not using it correctly, how can I incorporate this , I want to use custom icon in matstepper

@crisbeto
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom-icon is just used as an example, it's not an actual component.

@agrawalharsh
Copy link

@agrawalharsh agrawalharsh commented on f120014 Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, sorry it might not be the right forum to ask, then how can I incorporate this change, because this update might take some time to come in your future releases

@whimsicow
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has the ability to update stepper icons been released yet? If so, what is the implementation?

Thank you, @crisbeto

@crisbeto
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR hasn't been merged in yet.

Please sign in to comment.