Skip to content

feat(expansion-panel): allow for content to be rendered lazily #8243

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
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
2 changes: 2 additions & 0 deletions src/demo-app/expansion/expansion-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ <h1>Single Expansion Panel</h1>
<mat-panel-title>Panel Title</mat-panel-title>
</mat-expansion-panel-header>

<ng-template matExpansionPanelContent>
This is the content text that makes sense here.
<mat-checkbox>Trigger a ripple</mat-checkbox>
</ng-template>

<mat-action-row>
<button mat-button (click)="myPanel.expanded = false">CANCEL</button>
Expand Down
10 changes: 7 additions & 3 deletions src/lib/expansion/expansion-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {NgModule} from '@angular/core';
import {UNIQUE_SELECTION_DISPATCHER_PROVIDER} from '@angular/cdk/collections';
import {CdkAccordionModule} from '@angular/cdk/accordion';
import {A11yModule} from '@angular/cdk/a11y';
import {PortalModule} from '@angular/cdk/portal';
import {MatAccordion} from './accordion';
import {MatExpansionPanelContent} from './expansion-panel-content';
import {
MatExpansionPanel,
MatExpansionPanelActionRow,
Expand All @@ -25,14 +27,15 @@ import {


@NgModule({
imports: [CommonModule, A11yModule, CdkAccordionModule],
imports: [CommonModule, A11yModule, CdkAccordionModule, PortalModule],
exports: [
MatAccordion,
MatExpansionPanel,
MatExpansionPanelActionRow,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
MatExpansionPanelDescription
MatExpansionPanelDescription,
MatExpansionPanelContent,
],
declarations: [
MatExpansionPanelBase,
Expand All @@ -41,7 +44,8 @@ import {
MatExpansionPanelActionRow,
MatExpansionPanelHeader,
MatExpansionPanelTitle,
MatExpansionPanelDescription
MatExpansionPanelDescription,
MatExpansionPanelContent,
],
providers: [UNIQUE_SELECTION_DISPATCHER_PROVIDER]
})
Expand Down
20 changes: 20 additions & 0 deletions src/lib/expansion/expansion-panel-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @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';

/**
* Expansion panel content that will be rendered lazily
* after the panel is opened for the first time.
*/
@Directive({
selector: 'ng-template[matExpansionPanelContent]'
})
export class MatExpansionPanelContent {
constructor(public _template: TemplateRef<any>) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a quick question: Don't you use _ for private variables only?

Copy link
Contributor

Choose a reason for hiding this comment

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

They also use it for "internal use only" properties

}
7 changes: 5 additions & 2 deletions src/lib/expansion/expansion-panel.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<ng-content select="mat-expansion-panel-header"></ng-content>
<div [class.mat-expanded]="expanded" class="mat-expansion-panel-content"
[@bodyExpansion]="_getExpandedState()" [id]="id">
<div class="mat-expansion-panel-content"
[class.mat-expanded]="expanded"
[@bodyExpansion]="_getExpandedState()"
[id]="id">
<div class="mat-expansion-panel-body">
<ng-content></ng-content>
<ng-template [cdkPortalOutlet]="_portal"></ng-template>
</div>
<ng-content select="mat-action-row"></ng-content>
</div>
32 changes: 30 additions & 2 deletions src/lib/expansion/expansion-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ import {
Optional,
SimpleChanges,
ViewEncapsulation,
ViewContainerRef,
AfterContentInit,
ContentChild,
} from '@angular/core';
import {CdkAccordionItem} from '@angular/cdk/accordion';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
import {CanDisable, mixinDisabled} from '@angular/material/core';
import {TemplatePortal} from '@angular/cdk/portal';
import {Subject} from 'rxjs/Subject';
import {take} from 'rxjs/operators/take';
import {filter} from 'rxjs/operators/filter';
import {startWith} from 'rxjs/operators/startWith';
import {MatAccordion} from './accordion';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {MatExpansionPanelContent} from './expansion-panel-content';

/** Workaround for https://github.com/angular/angular/issues/17849 */
export const _CdkAccordionItem = CdkAccordionItem;
Expand Down Expand Up @@ -91,7 +99,7 @@ export type MatExpansionPanelState = 'expanded' | 'collapsed';
],
})
export class MatExpansionPanel extends _MatExpansionPanelMixinBase
implements CanDisable, OnChanges, OnDestroy {
implements CanDisable, AfterContentInit, OnChanges, OnDestroy {

/** Whether the toggle indicator should be hidden. */
@Input()
Expand All @@ -109,9 +117,16 @@ export class MatExpansionPanel extends _MatExpansionPanelMixinBase
/** Optionally defined accordion the expansion panel belongs to. */
accordion: MatAccordion;

/** Content that will be rendered lazily. */
@ContentChild(MatExpansionPanelContent) _lazyContent: MatExpansionPanelContent;

/** Portal holding the user's content. */
_portal: TemplatePortal<any>;

constructor(@Optional() @Host() accordion: MatAccordion,
_changeDetectorRef: ChangeDetectorRef,
_uniqueSelectionDispatcher: UniqueSelectionDispatcher) {
_uniqueSelectionDispatcher: UniqueSelectionDispatcher,
private _viewContainerRef: ViewContainerRef) {
super(accordion, _changeDetectorRef, _uniqueSelectionDispatcher);
this.accordion = accordion;
}
Expand All @@ -137,6 +152,19 @@ export class MatExpansionPanel extends _MatExpansionPanelMixinBase
return this.expanded ? 'expanded' : 'collapsed';
}

ngAfterContentInit() {
if (this._lazyContent) {
// Render the content as soon as the panel becomes open.
this.opened.pipe(
startWith(null!),
Copy link
Member

Choose a reason for hiding this comment

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

Why does this start with never?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the opened stream won't emit if the panel is open by default. This ensures that we render any panels that are open on load.

filter(() => this.expanded && !this._portal),
take(1)
).subscribe(() => {
this._portal = new TemplatePortal<any>(this._lazyContent._template, this._viewContainerRef);
});
}
}

ngOnChanges(changes: SimpleChanges) {
this._inputChanges.next(changes);
}
Expand Down
18 changes: 17 additions & 1 deletion src/lib/expansion/expansion.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,25 @@ panel can be expanded at a given time:
</mat-accordion>
```

### Lazy rendering
By default, the expansion panel content will be initialized even when the panel is closed.
To instead defer initialization until the panel is open, the content should be provided as
an `ng-template`:
```html
<mat-expansion-panel>
<mat-expansion-panel-header>
This is the expansion title
</mat-expansion-panel-header>

<ng-template matExpansionPanelContent>
Some deferred content
</ng-template>
</mat-expansion-panel>
```

### Accessibility
The expansion-panel aims to mimic the experience of the native `<details>` and `<summary>` elements.
The expansion panel header has `role="button"` and also the attribute `aria-controls` with the
The expansion panel header has `role="button"` and also the attribute `aria-controls` with the
expansion panel's id as value.

The expansion panel headers are buttons. Users can use the keyboard to activate the expansion panel
Expand Down
57 changes: 55 additions & 2 deletions src/lib/expansion/expansion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe('MatExpansionPanel', () => {
declarations: [
PanelWithContent,
PanelWithContentInNgIf,
PanelWithCustomMargin
PanelWithCustomMargin,
LazyPanelWithContent,
LazyPanelOpenOnLoad,
],
});
TestBed.compileComponents();
Expand All @@ -35,6 +37,29 @@ describe('MatExpansionPanel', () => {
expect(contentEl.classes['mat-expanded']).toBeTruthy();
});

it('should be able to render panel content lazily', fakeAsync(() => {
let fixture = TestBed.createComponent(LazyPanelWithContent);
let content = fixture.debugElement.query(By.css('.mat-expansion-panel-content')).nativeElement;
fixture.detectChanges();

expect(content.textContent.trim()).toBe('', 'Expected content element to be empty.');

fixture.componentInstance.expanded = true;
fixture.detectChanges();

expect(content.textContent.trim())
.toContain('Some content', 'Expected content to be rendered.');
}));

it('should render the content for a lazy-loaded panel that is opened on init', fakeAsync(() => {
let fixture = TestBed.createComponent(LazyPanelOpenOnLoad);
let content = fixture.debugElement.query(By.css('.mat-expansion-panel-content')).nativeElement;
fixture.detectChanges();

expect(content.textContent.trim())
.toContain('Some content', 'Expected content to be rendered.');
}));

it('emit correct events for change in panel expanded state', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.componentInstance.expanded = true;
Expand All @@ -61,12 +86,13 @@ describe('MatExpansionPanel', () => {

it('should not be able to focus content while closed', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithContent);
const button = fixture.debugElement.query(By.css('button')).nativeElement;

fixture.componentInstance.expanded = true;
fixture.detectChanges();
tick(250);

const button = fixture.debugElement.query(By.css('button')).nativeElement;

button.focus();
expect(document.activeElement).toBe(button, 'Expected button to start off focusable.');

Expand Down Expand Up @@ -260,3 +286,30 @@ class PanelWithContentInNgIf {
class PanelWithCustomMargin {
expanded: boolean = false;
}

@Component({
template: `
<mat-expansion-panel [expanded]="expanded">
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>

<ng-template matExpansionPanelContent>
<p>Some content</p>
<button>I am a button</button>
</ng-template>
</mat-expansion-panel>`
})
class LazyPanelWithContent {
expanded = false;
}

@Component({
template: `
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>

<ng-template matExpansionPanelContent>
<p>Some content</p>
</ng-template>
</mat-expansion-panel>`
})
class LazyPanelOpenOnLoad {}
3 changes: 1 addition & 2 deletions src/lib/expansion/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ export * from './expansion-module';
export * from './accordion';
export * from './expansion-panel';
export * from './expansion-panel-header';


export * from './expansion-panel-content';