Skip to content

Commit e89c09b

Browse files
committed
docs: snippet-based examples not showing after clicking away and coming back (#31415)
Fixes that examples based on snippets (e.g. first example for autocomplete) weren't showing up if the user lands on the page, clicks to another tab and then comes back. The problem was that the timing is different on the second run, because we hit the cached rather than fetching the content. These changes resolve it by switching the component's internal state to signals. (cherry picked from commit d820a26)
1 parent f48c8ad commit e89c09b

File tree

4 files changed

+134
-102
lines changed

4 files changed

+134
-102
lines changed

docs/src/app/shared/doc-viewer/doc-viewer.spec.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ describe('DocViewer', () => {
9595

9696
http.expectOne(testUrl).flush(FAKE_DOCS[testUrl]);
9797

98-
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
99-
expect(exampleViewer.componentInstance.file).toBe('some-example.html');
100-
expect(exampleViewer.componentInstance.showCompactToggle).toBeTrue();
101-
expect(exampleViewer.componentInstance.region).toBe('some-region');
102-
expect(exampleViewer.componentInstance.view).toBe('snippet');
98+
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer))
99+
.componentInstance as ExampleViewer;
100+
expect(exampleViewer.file()).toBe('some-example.html');
101+
expect(exampleViewer.showCompactToggle()).toBeTrue();
102+
expect(exampleViewer.region()).toBe('some-region');
103+
expect(exampleViewer.view()).toBe('snippet');
103104
});
104105

105106
it('should instantiate example viewer in demo view', () => {
@@ -111,10 +112,11 @@ describe('DocViewer', () => {
111112

112113
http.expectOne(testUrl).flush(FAKE_DOCS[testUrl]);
113114

114-
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
115-
expect(exampleViewer.componentInstance.file).toBeUndefined();
116-
expect(exampleViewer.componentInstance.showCompactToggle).toBeFalse();
117-
expect(exampleViewer.componentInstance.view).toBe('demo');
115+
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer))
116+
.componentInstance as ExampleViewer;
117+
expect(exampleViewer.file()).toBeUndefined();
118+
expect(exampleViewer.showCompactToggle()).toBeFalse();
119+
expect(exampleViewer.view()).toBe('demo');
118120
});
119121

120122
it('should instantiate example viewer in snippet view with whole snippet', () => {
@@ -126,10 +128,11 @@ describe('DocViewer', () => {
126128

127129
http.expectOne(testUrl).flush(FAKE_DOCS[testUrl]);
128130

129-
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
130-
expect(exampleViewer.componentInstance.file).toBe('whole-snippet-example.ts');
131-
expect(exampleViewer.componentInstance.showCompactToggle).toBeTrue();
132-
expect(exampleViewer.componentInstance.view).toBe('snippet');
131+
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer))
132+
.componentInstance as ExampleViewer;
133+
expect(exampleViewer.file()).toBe('whole-snippet-example.ts');
134+
expect(exampleViewer.showCompactToggle()).toBeTrue();
135+
expect(exampleViewer.view()).toBe('snippet');
133136
});
134137

135138
it('should show error message when doc not found', () => {

docs/src/app/shared/doc-viewer/doc-viewer.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ViewContainerRef,
3131
input,
3232
inject,
33+
Type,
3334
} from '@angular/core';
3435
import {Observable, Subscription} from 'rxjs';
3536
import {shareReplay, take, tap} from 'rxjs/operators';
@@ -113,17 +114,17 @@ export class DocViewer implements OnDestroy {
113114
if (file) {
114115
// if the html div has field `file` then it should be in compact view to show the code
115116
// snippet
116-
exampleViewerComponent.view = 'snippet';
117-
exampleViewerComponent.showCompactToggle = true;
118-
exampleViewerComponent.file = file;
117+
exampleViewerComponent.view.set('snippet');
118+
exampleViewerComponent.showCompactToggle.set(true);
119+
exampleViewerComponent.file.set(file);
119120
if (region) {
120121
// `region` should only exist when `file` exists but not vice versa
121122
// It is valid for embedded example snippets to show the whole file (esp short files)
122-
exampleViewerComponent.region = region;
123+
exampleViewerComponent.region.set(region);
123124
}
124125
} else {
125126
// otherwise it is an embedded demo
126-
exampleViewerComponent.view = 'demo';
127+
exampleViewerComponent.view.set('demo');
127128
}
128129
}
129130

@@ -175,7 +176,7 @@ export class DocViewer implements OnDestroy {
175176
}
176177

177178
/** Instantiate a ExampleViewer for each example. */
178-
private _loadComponents(componentName: string, componentClass: any) {
179+
private _loadComponents(componentName: string, componentClass: Type<ExampleViewer | HeaderLink>) {
179180
const exampleElements = this._elementRef.nativeElement.querySelectorAll(`[${componentName}]`);
180181

181182
[...exampleElements].forEach((element: Element) => {
@@ -185,9 +186,14 @@ export class DocViewer implements OnDestroy {
185186
const portalHost = new DomPortalOutlet(element, this._appRef, this._injector);
186187
const examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
187188
const exampleViewer = portalHost.attach(examplePortal);
188-
const exampleViewerComponent = exampleViewer.instance as ExampleViewer;
189-
if (example !== null) {
190-
DocViewer._initExampleViewer(exampleViewerComponent, example, file, region);
189+
const exampleViewerComponent = exampleViewer.instance;
190+
if (example !== null && componentClass === ExampleViewer) {
191+
DocViewer._initExampleViewer(
192+
exampleViewerComponent as ExampleViewer,
193+
example,
194+
file,
195+
region,
196+
);
191197
}
192198
this._portalHosts.push(portalHost);
193199
});

docs/src/app/shared/example-viewer/example-viewer.html

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
@let exampleData = this.exampleData();
2+
@let fileUrl = this.fileUrl();
3+
14
<div class="docs-example-viewer-wrapper">
2-
@if (view === 'snippet') {
5+
@if (view() === 'snippet') {
36
<div class="docs-example-viewer-source-compact">
47
<div class="docs-button-bar">
58
<button mat-icon-button type="button" (click)="copySource(snippet())"
@@ -37,7 +40,7 @@
3740
<mat-icon>link</mat-icon>
3841
</button>
3942

40-
@if (showCompactToggle) {
43+
@if (showCompactToggle()) {
4144
<button mat-icon-button
4245
(click)="toggleCompactView()"
4346
matTooltip="View snippet only"
@@ -52,37 +55,39 @@
5255
}
5356

5457
<button mat-icon-button type="button" (click)="toggleSourceView()"
55-
[matTooltip]="view === 'demo' ? 'View code' : 'Hide code'" aria-label="View source">
58+
[matTooltip]="view() === 'demo' ? 'View code' : 'Hide code'" aria-label="View source">
5659
<mat-icon>code</mat-icon>
5760
</button>
5861

5962
<stackblitz-button [example]="example"></stackblitz-button>
6063
</div>
6164

62-
@if (view === 'full') {
65+
@if (view() === 'full') {
6366
<div class="docs-example-viewer-source">
6467
<mat-tab-group animationDuration="0ms" [(selectedIndex)]="selectedTab" mat-stretch-tabs="false">
65-
@for (tabName of _getExampleTabNames(); track tabName) {
68+
@for (tabName of _exampleTabNames(); track tabName) {
6669
<mat-tab [label]="tabName">
6770
<div class="docs-button-bar">
68-
<button mat-icon-button type="button" (click)="copySource(snippet(), selectedTab)"
71+
<button mat-icon-button type="button" (click)="copySource(snippet(), selectedTab())"
6972
class="docs-example-source-copy docs-example-button" matTooltip="Copy example source"
7073
title="Copy example source" aria-label="Copy example source to clipboard">
7174
<mat-icon>content_copy</mat-icon>
7275
</button>
7376
</div>
74-
<code-snippet [source]="exampleTabs[tabName]"></code-snippet>
77+
<code-snippet [source]="exampleTabs()[tabName]"></code-snippet>
7578
</mat-tab>
7679
}
7780
</mat-tab-group>
7881
</div>
7982
}
8083
}
8184

82-
@if (view !== 'snippet') {
85+
@if (view() !== 'snippet') {
8386
<div class="docs-example-viewer-body">
84-
@if (_exampleComponentType && !example?.includes('harness')) {
85-
<ng-template [ngComponentOutlet]="_exampleComponentType"/>
87+
@let componentType = _exampleComponentType();
88+
89+
@if (componentType && !example?.includes('harness')) {
90+
<ng-template [ngComponentOutlet]="componentType"/>
8691
} @else {
8792
<div>This example contains tests. Open in Stackblitz to run the tests.</div>
8893
}

0 commit comments

Comments
 (0)