Skip to content

Commit

Permalink
feat(stepper): allow for content to be rendered lazily
Browse files Browse the repository at this point in the history
Adds the `matStepContent` directive that allows consumers to defer rendering the content of a step until it is opened for the first time.

Fixes angular#12339.
  • Loading branch information
crisbeto committed Apr 14, 2019
1 parent f6edba3 commit f46c948
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class CdkStep implements OnChanges {

/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */
constructor(
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper,
@Inject(forwardRef(() => CdkStepper)) protected _stepper: CdkStepper,
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
this._stepperOptions = stepperOptions ? stepperOptions : {};
this._displayDefaultIndicatorType = this._stepperOptions.displayDefaultIndicatorType !== false;
Expand Down
1 change: 1 addition & 0 deletions src/lib/stepper/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './step-header';
export * from './stepper-intl';
export * from './stepper-animations';
export * from './stepper-icon';
export * from './step-content';
19 changes: 19 additions & 0 deletions src/lib/stepper/step-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @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, TemplateRef} from '@angular/core';

/**
* Content for a `mat-step` that will be rendered lazily.
*/
@Directive({
selector: 'ng-template[matStepContent]'
})
export class MatStepContent {
constructor(public _template: TemplateRef<any>) {}
}
5 changes: 4 additions & 1 deletion src/lib/stepper/step.html
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
<ng-template><ng-content></ng-content></ng-template>
<ng-template>
<ng-content></ng-content>
<ng-template [cdkPortalOutlet]="_portal"></ng-template>
</ng-template>
3 changes: 3 additions & 0 deletions src/lib/stepper/stepper-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './s
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
import {MatStepperIcon} from './stepper-icon';
import {MAT_STEPPER_INTL_PROVIDER} from './stepper-intl';
import {MatStepContent} from './step-content';


@NgModule({
Expand All @@ -42,6 +43,7 @@ import {MAT_STEPPER_INTL_PROVIDER} from './stepper-intl';
MatStepperPrevious,
MatStepHeader,
MatStepperIcon,
MatStepContent,
],
declarations: [
MatHorizontalStepper,
Expand All @@ -53,6 +55,7 @@ import {MAT_STEPPER_INTL_PROVIDER} from './stepper-intl';
MatStepperPrevious,
MatStepHeader,
MatStepperIcon,
MatStepContent,
],
providers: [MAT_STEPPER_INTL_PROVIDER, ErrorStateMatcher],
})
Expand Down
7 changes: 7 additions & 0 deletions src/lib/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ The stepper can now show error states by simply providing the `showError` option

<!-- example(stepper-errors) -->

### Lazy rendering
By default, the stepper will render all of it's content when it's initialized. If you have some
content that you want to want to defer until the particular step is opened, you can put it inside
an `ng-template` with the `matStepContent` attribute.

<!-- example(stepper-lazy-content) -->

### Keyboard interaction
- <kbd>LEFT_ARROW</kbd>: Focuses the previous step header
- <kbd>RIGHT_ARROW</kbd>: Focuses the next step header
Expand Down
61 changes: 61 additions & 0 deletions src/lib/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,44 @@ describe('MatStepper', () => {
expect(stepper._getIndicatorType(1)).toBe(STEP_STATE.EDIT);
});
});

describe('stepper with lazy content', () => {
it('should render the content of the selected step on init', () => {
const fixture = createComponent(StepperWithLazyContent);
const element = fixture.nativeElement;
fixture.componentInstance.selectedIndex = 1;
fixture.detectChanges();

expect(element.textContent).not.toContain('Step 1 content');
expect(element.textContent).toContain('Step 2 content');
expect(element.textContent).not.toContain('Step 3 content');
});

it('should render the content of steps when the user navigates to them', () => {
const fixture = createComponent(StepperWithLazyContent);
const element = fixture.nativeElement;
fixture.componentInstance.selectedIndex = 0;
fixture.detectChanges();

expect(element.textContent).toContain('Step 1 content');
expect(element.textContent).not.toContain('Step 2 content');
expect(element.textContent).not.toContain('Step 3 content');

fixture.componentInstance.selectedIndex = 1;
fixture.detectChanges();

expect(element.textContent).toContain('Step 1 content');
expect(element.textContent).toContain('Step 2 content');
expect(element.textContent).not.toContain('Step 3 content');

fixture.componentInstance.selectedIndex = 2;
fixture.detectChanges();

expect(element.textContent).toContain('Step 1 content');
expect(element.textContent).toContain('Step 2 content');
expect(element.textContent).toContain('Step 3 content');
});
});
});

/** Asserts that keyboard interaction works correctly. */
Expand Down Expand Up @@ -1498,3 +1536,26 @@ class StepperWithAriaInputs {
ariaLabel: string;
ariaLabelledby: string;
}

@Component({
template: `
<mat-vertical-stepper [selectedIndex]="selectedIndex">
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
<ng-template matStepContent>Step 1 content</ng-template>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 2</ng-template>
<ng-template matStepContent>Step 2 content</ng-template>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 3</ng-template>
<ng-template matStepContent>Step 3 content</ng-template>
</mat-step>
</mat-vertical-stepper>
`
})
class StepperWithLazyContent {
selectedIndex = 0;
}

40 changes: 36 additions & 4 deletions src/lib/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,28 @@ import {
forwardRef,
Inject,
Input,
OnDestroy,
Optional,
Output,
QueryList,
SkipSelf,
TemplateRef,
ViewChildren,
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
import {DOCUMENT} from '@angular/common';
import {ErrorStateMatcher} from '@angular/material/core';
import {Subject} from 'rxjs';
import {takeUntil, distinctUntilChanged} from 'rxjs/operators';
import {TemplatePortal} from '@angular/cdk/portal';
import {Subject, Subscription} from 'rxjs';
import {takeUntil, distinctUntilChanged, map, startWith} from 'rxjs/operators';

import {MatStepHeader} from './step-header';
import {MatStepLabel} from './step-label';
import {matStepperAnimations} from './stepper-animations';
import {MatStepperIcon, MatStepperIconContext} from './stepper-icon';
import {MatStepContent} from './step-content';

@Component({
moduleId: module.id,
Expand All @@ -56,17 +60,45 @@ import {MatStepperIcon, MatStepperIconContext} from './stepper-icon';
exportAs: 'matStep',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatStep extends CdkStep implements ErrorStateMatcher {
export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentInit, OnDestroy {
private _isSelected = Subscription.EMPTY;

/** Content for step label given by `<ng-template matStepLabel>`. */
@ContentChild(MatStepLabel, {static: false}) stepLabel: MatStepLabel;

/** Content that will be rendered lazily. */
@ContentChild(MatStepContent, {static: false}) _lazyContent: MatStepContent;

/** Currently-attached portal containing the lazy content. */
_portal: TemplatePortal;

/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */
/** @breaking-change 9.0.0 _viewContainerRef parameter to become required. */
constructor(@Inject(forwardRef(() => MatStepper)) stepper: MatStepper,
@SkipSelf() private _errorStateMatcher: ErrorStateMatcher,
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions,
private _viewContainerRef?: ViewContainerRef) {
super(stepper, stepperOptions);
}

ngAfterContentInit() {
/** @breaking-change 9.0.0 Null check for _viewContainerRef to be removed. */
if (this._viewContainerRef) {
this._isSelected = this._stepper.selectionChange.pipe(
map(event => event.selectedStep === this),
startWith(this._stepper.selected === this)
).subscribe(isSelected => {
if (isSelected && this._lazyContent && !this._portal) {
this._portal = new TemplatePortal(this._lazyContent._template, this._viewContainerRef!);
}
});
}
}

ngOnDestroy() {
this._isSelected.unsubscribe();
}

/** Custom error state matcher that additionally checks for validity of interacted form. */
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this._errorStateMatcher.isErrorState(control, form);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/** No CSS for this example */
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<mat-vertical-stepper>
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
<ng-template matStepContent>
<p>This content was rendered lazily</p>
<button mat-button matStepperNext>Next</button>
</ng-template>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 2</ng-template>
<ng-template matStepContent>
<p>This content was also rendered lazily</p>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</ng-template>
</mat-step>
<mat-step>
<ng-template matStepLabel>Step 3</ng-template>
<p>This content was rendered eagerly</p>
<button mat-button matStepperPrevious>Back</button>
</mat-step>
</mat-vertical-stepper>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Component} from '@angular/core';

/**
* @title Stepper lazy content rendering
*/
@Component({
selector: 'stepper-lazy-content-example',
templateUrl: 'stepper-lazy-content-example.html',
styleUrls: ['stepper-lazy-content-example.css'],
})
export class StepperLazyContentExample {}
1 change: 1 addition & 0 deletions tools/public_api_guard/cdk/stepper.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export declare class CdkStep implements OnChanges {
_displayDefaultIndicatorType: boolean;
_showError: boolean;
protected _stepper: CdkStepper;
ariaLabel: string;
ariaLabelledby: string;
completed: boolean;
Expand Down
13 changes: 11 additions & 2 deletions tools/public_api_guard/lib/stepper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ export declare class MatHorizontalStepper extends MatStepper {
labelPosition: 'bottom' | 'end';
}

export declare class MatStep extends CdkStep implements ErrorStateMatcher {
export declare class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentInit, OnDestroy {
_lazyContent: MatStepContent;
_portal: TemplatePortal;
stepLabel: MatStepLabel;
constructor(stepper: MatStepper, _errorStateMatcher: ErrorStateMatcher, stepperOptions?: StepperOptions);
constructor(stepper: MatStepper, _errorStateMatcher: ErrorStateMatcher, stepperOptions?: StepperOptions, _viewContainerRef?: ViewContainerRef | undefined);
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean;
ngAfterContentInit(): void;
ngOnDestroy(): void;
}

export declare class MatStepContent {
_template: TemplateRef<any>;
constructor(_template: TemplateRef<any>);
}

export declare class MatStepHeader extends CdkStepHeader implements OnDestroy {
Expand Down

0 comments on commit f46c948

Please sign in to comment.