Skip to content

Commit cffcadc

Browse files
authoredMay 6, 2020
feat: compact view for example components to replace static code snippets (#756)
1 parent 866e838 commit cffcadc

11 files changed

+159
-52
lines changed
 

‎material.angular.io/src/app/pages/component-viewer/component-examples.html

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44
</span>
55
<example-viewer
66
*ngFor="let example of docItem?.examples"
7-
[example]="example"></example-viewer>
7+
[example]="example"
8+
[showCompactToggle]="false"
9+
[view]="'collapsed'"></example-viewer>
810
</ng-container>

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer-module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {CommonModule} from '@angular/common';
1111
import {NgModule} from '@angular/core';
1212
import {HeaderLink} from './header-link';
1313
import {CopierService} from '../copier/copier.service';
14+
import {CodeSnippet} from '../example-viewer/code-snippet';
1415

1516

1617
// ExampleViewer is included in the DocViewerModule because they have a circular dependency.
@@ -26,7 +27,7 @@ import {CopierService} from '../copier/copier.service';
2627
StackBlitzButtonModule
2728
],
2829
providers: [CopierService],
29-
declarations: [DocViewer, ExampleViewer, HeaderLink],
30+
declarations: [DocViewer, ExampleViewer, HeaderLink, CodeSnippet],
3031
entryComponents: [ExampleViewer, HeaderLink],
3132
exports: [DocViewer, ExampleViewer, HeaderLink],
3233
})

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer.spec.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ describe('DocViewer', () => {
7070
expect(docViewer.nativeElement.innerHTML).toContain('id="my-header"');
7171
});
7272

73+
it('should show part of example source code', () => {
74+
const fixture = TestBed.createComponent(DocViewerTestComponent);
75+
const testUrl = 'http://material.angular.io/example-html.html';
76+
77+
fixture.componentInstance.documentUrl = testUrl;
78+
fixture.componentInstance.lines = [0, 1];
79+
fixture.detectChanges();
80+
81+
http.expectOne(testUrl).flush(FAKE_DOCS[testUrl]);
82+
83+
const docViewer = fixture.debugElement.query(By.directive(DocViewer));
84+
expect(docViewer.componentInstance.textContent).toBe('line 1');
85+
});
86+
7387
it('should show error message when doc not found', () => {
7488
spyOn(console, 'log');
7589

@@ -99,10 +113,11 @@ describe('DocViewer', () => {
99113

100114
@Component({
101115
selector: 'test',
102-
template: `<doc-viewer [documentUrl]="documentUrl"></doc-viewer>`,
116+
template: `<doc-viewer [documentUrl]="documentUrl" [lines]="lines"></doc-viewer>`,
103117
})
104118
class DocViewerTestComponent {
105119
documentUrl = 'http://material.angular.io/simple-doc.html';
120+
lines: [number, number];
106121
}
107122

108123
const FAKE_DOCS: {[key: string]: string} = {
@@ -112,4 +127,8 @@ const FAKE_DOCS: {[key: string]: string} = {
112127
<div material-docs-example="some-example"></div>`,
113128
'http://material.angular.io/doc-with-links.html': `<a href="#test">Test link</a>`,
114129
'http://material.angular.io/doc-with-element-ids.html': `<h4 id="my-header">Header</h4>`,
130+
'http://material.angular.io/example-html.html':
131+
`<span>line 1</span>
132+
<span>line 2</span>
133+
<span>line 3</span>`,
115134
};

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export class DocViewer implements OnDestroy {
3838
}
3939
}
4040

41+
@Input() lines: readonly [number, number];
42+
4143
@Output() contentRendered = new EventEmitter<HTMLElement>();
4244

4345
/** The document text. It should not be HTML encoded. */
@@ -78,9 +80,13 @@ export class DocViewer implements OnDestroy {
7880
const absoluteUrl = `${location.pathname}#${fragmentUrl}`;
7981
return `href="${this._domSanitizer.sanitize(SecurityContext.URL, absoluteUrl)}"`;
8082
});
81-
8283
this._elementRef.nativeElement.innerHTML = rawDocument;
8384
this.textContent = this._elementRef.nativeElement.textContent;
85+
if (this.lines) {
86+
this._elementRef.nativeElement.innerHTML =
87+
rawDocument.split('\n').slice(this.lines[0], this.lines[1]).join('\n');
88+
this.textContent = this._elementRef.nativeElement.textContent;
89+
}
8490

8591
this._loadComponents('material-docs-example', ExampleViewer);
8692
this._loadComponents('header-link', HeaderLink);
@@ -105,14 +111,16 @@ export class DocViewer implements OnDestroy {
105111
const exampleElements =
106112
this._elementRef.nativeElement.querySelectorAll(`[${componentName}]`);
107113

108-
Array.prototype.slice.call(exampleElements).forEach((element: Element) => {
114+
[...exampleElements].forEach((element: Element) => {
109115
const example = element.getAttribute(componentName);
110116
const portalHost = new DomPortalOutlet(
111117
element, this._componentFactoryResolver, this._appRef, this._injector);
112118
const examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
113119
const exampleViewer = portalHost.attach(examplePortal);
114120
if (example !== null) {
115-
(exampleViewer.instance as ExampleViewer).example = example;
121+
const exampleViewerComponent = exampleViewer.instance as ExampleViewer;
122+
exampleViewerComponent.example = example;
123+
exampleViewerComponent.view = 'collapsed';
116124
}
117125

118126
this._portalHosts.push(portalHost);

‎material.angular.io/src/app/shared/example-viewer/_example-viewer-theme.scss

+11-6
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,16 @@
77

88
example-viewer {
99
.docs-example-viewer-wrapper {
10-
border: 1px solid rgba(mat-color($foreground, secondary-text), 0.03);
11-
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24), 0 0 2px rgba(0, 0, 0, 0.12);
10+
border: 1px solid rgba(mat-color($foreground, secondary-text), 0.2);
1211
margin: 4px;
1312
}
1413

1514
.docs-example-viewer-title {
1615
color: mat-color($foreground, secondary-text);
17-
background: rgba(mat-color($foreground, secondary-text), 0.03);
1816
}
1917

20-
.docs-example-source-copy {
18+
.docs-example-button {
2119
color: mat-color($foreground, hint-text);
22-
right: 8px;
2320

2421
[dir='rtl'] & {
2522
right: auto;
@@ -28,8 +25,16 @@
2825
}
2926

3027
.docs-example-source {
31-
border-bottom: 1px solid mat-color($foreground, divider);
3228
overflow: auto;
3329
}
30+
31+
code-snippet {
32+
background: rgba(mat-color($foreground, secondary-text), 0.01);
33+
}
34+
35+
.docs-example-viewer-source code-snippet {
36+
border-bottom: 1px solid mat-color($foreground, divider);
37+
}
3438
}
3539
}
40+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="docs-example-source-wrapper">
2+
<pre class="docs-example-source"><doc-viewer #viewer [documentUrl]="source" [lines]="lines"></doc-viewer></pre>
3+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {ChangeDetectionStrategy, Component, Input, ViewChild} from '@angular/core';
2+
import {DocViewer} from '../doc-viewer/doc-viewer';
3+
4+
@Component({
5+
selector: 'code-snippet',
6+
templateUrl: './code-snippet.html',
7+
styleUrls: ['./example-viewer.scss'],
8+
changeDetection: ChangeDetectionStrategy.OnPush
9+
})
10+
export class CodeSnippet {
11+
@Input() source: string;
12+
@Input() lines: readonly [number, number];
13+
@ViewChild('viewer') viewer: DocViewer;
14+
}

‎material.angular.io/src/app/shared/example-viewer/example-viewer.html

+40-16
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,61 @@
11
<div class="docs-example-viewer-wrapper">
2-
<div class="docs-example-viewer-title">
2+
<div class="docs-example-viewer-title" *ngIf="view !== 'compact'">
33
<div class="docs-example-viewer-title-spacer">{{exampleData?.title}}</div>
44

5-
<button mat-icon-button type="button" (click)="toggleSourceView()"
6-
[matTooltip]="'View ' + exampleData?.title + ' source'" aria-label="View source">
5+
<button mat-icon-button type="button" (click)="toggleCompactView()" [matTooltip]="'View snippet only'"
6+
aria-label="View less" *ngIf="showCompactToggle">
77
<mat-icon>
8-
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fit="" preserveAspectRatio="xMidYMid meet" focusable="false">
9-
<path fill="none" d="M0 0h24v24H0V0z"></path>
10-
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"></path>
11-
</svg>
8+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" focusable="false">
9+
<path
10+
d="M15.41,10H20v2h-8V4h2v4.59L20.59,2L22,3.41L15.41,10z M4,12v2h4.59L2,20.59L3.41,22L10,15.41V20h2v-8H4z"/>
11+
</svg>
1212
</mat-icon>
1313
</button>
1414

15+
<button mat-icon-button type="button" (click)="toggleSourceView()"
16+
[matTooltip]="view==='collapsed' ? 'View code' : 'Hide code'" aria-label="View source">
17+
<mat-icon>code</mat-icon>
18+
</button>
19+
1520
<stack-blitz-button [example]="example"></stack-blitz-button>
1621
</div>
1722

18-
<div class="docs-example-viewer-source" *ngIf="showSource">
19-
<mat-tab-group>
23+
<div class="docs-example-viewer-source" *ngIf="view === 'full'">
24+
<mat-tab-group animationDuration="0ms">
2025
<mat-tab *ngFor="let tabName of _getExampleTabNames()" [label]="tabName">
21-
<div class="docs-example-source-wrapper">
22-
<button mat-icon-button type="button" class="docs-example-source-copy"
23-
title="Copy example source" aria-label="Copy example source to clipboard"
24-
(click)="copySource(viewer.textContent)">
26+
<div class="button-bar">
27+
<button mat-icon-button type="button" (click)="copySource(snippet.first.viewer.textContent)"
28+
class="docs-example-source-copy docs-example-button" [matTooltip]="'Copy example source'"
29+
title="Copy example source" aria-label="Copy example source to clipboard">
2530
<mat-icon>content_copy</mat-icon>
2631
</button>
27-
<pre class="docs-example-source"><doc-viewer
28-
#viewer [documentUrl]="exampleTabs[tabName]"></doc-viewer></pre>
2932
</div>
33+
<code-snippet [source]="exampleTabs[tabName]"></code-snippet>
3034
</mat-tab>
3135
</mat-tab-group>
3236
</div>
3337

34-
<div class="docs-example-viewer-body">
38+
<div class="docs-example-viewer-source-compact" *ngIf="view === 'compact'">
39+
<div class="button-bar">
40+
<button mat-icon-button type="button" (click)="copySource(snippet.first.viewer.textContent)"
41+
class="docs-example-source-copy docs-example-button" [matTooltip]="'Copy snippet'"
42+
title="Copy example source" aria-label="Copy example source to clipboard">
43+
<mat-icon>content_copy</mat-icon>
44+
</button>
45+
<button mat-icon-button type="button" (click)="toggleCompactView()"
46+
class="docs-example-compact-toggle docs-example-button" [matTooltip]="'View full example'"
47+
aria-label="View less">
48+
<mat-icon>
49+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" focusable="false">
50+
<polygon points="13,3 13,5 17.59,5 5,17.59 5,13 3,13 3,21 11,21 11,19 6.41,19 19,6.41 19,11 21,11 21,3"/>
51+
</svg>
52+
</mat-icon>
53+
</button>
54+
</div>
55+
<code-snippet [source]="file" [lines]="lines" *ngIf="file !== 'demo'"></code-snippet>
56+
</div>
57+
58+
<div class="docs-example-viewer-body" *ngIf="view !== 'compact' || file === 'demo'">
3559
<ng-template *ngIf="_exampleComponentType && _exampleModuleFactory"
3660
[ngComponentOutlet]="_exampleComponentType"
3761
[ngComponentOutletNgModuleFactory]="_exampleModuleFactory"></ng-template>

‎material.angular.io/src/app/shared/example-viewer/example-viewer.scss

+16-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
}
55

66
.docs-example-viewer-wrapper {
7+
border-radius: 4px;
8+
margin: 24px 0;
79
h3 {
810
margin-top: 10px;
911
}
@@ -14,30 +16,30 @@
1416
align-items: center;
1517
display: flex;
1618
justify-content: center;
17-
padding: 8px 20px;
19+
padding: 8px 8px 8px 16px;
1820
}
1921

2022
.docs-example-viewer-title-spacer {
2123
flex: 1 1 auto;
2224
}
2325

24-
.docs-example-source-copy {
25-
position: absolute;
26-
top: 8px;
27-
display: none;
26+
.docs-example-viewer-body {
27+
padding: 30px;
2828
}
2929

30-
.docs-example-source-wrapper:hover {
31-
.docs-example-source-copy {
32-
display: inline-block;
33-
}
30+
.button-bar {
31+
float: right;
32+
padding: 8px;
3433
}
3534

36-
.docs-example-source {
37-
padding: 0 30px 10px 30px;
38-
min-height: 150px;
35+
code-snippet {
36+
padding: 20px;
3937
}
4038

41-
.docs-example-viewer-body {
42-
padding: 30px;
39+
.docs-example-source {
40+
// TODO(annieyw): remove when standalone snippets are removed
41+
padding: 0;
42+
margin: 0;
43+
border: none;
44+
background: none;
4345
}

‎material.angular.io/src/app/shared/example-viewer/example-viewer.spec.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,14 @@ describe('ExampleViewer', () => {
4646
component = fixture.componentInstance;
4747
});
4848

49-
it('should toggle showSource boolean', async(() => {
49+
it('should toggle between the 3 views', async(() => {
5050
fixture.detectChanges();
51-
expect(component.showSource).toBe(false);
51+
component.view = 'compact';
52+
expect(component.view).toBe('compact');
53+
component.toggleCompactView();
54+
expect(component.view).toBe('full');
5255
component.toggleSourceView();
53-
expect(component.showSource).toBe(true);
56+
expect(component.view).toBe('collapsed');
5457
}));
5558

5659
it('should set and return example properly', async(() => {
@@ -114,7 +117,7 @@ describe('ExampleViewer', () => {
114117
beforeEach(() => {
115118
// Open source view
116119
component.example = exampleKey;
117-
component.showSource = true;
120+
component.view = 'full';
118121
fixture.detectChanges();
119122

120123
for (const url of Object.keys(FAKE_DOCS)) {

‎material.angular.io/src/app/shared/example-viewer/example-viewer.ts

+32-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import {Component, Input, NgModuleFactory, Type, ɵNgModuleFactory} from '@angular/core';
1+
import {
2+
Component,
3+
Input,
4+
NgModuleFactory, QueryList,
5+
Type,
6+
ViewChildren,
7+
ɵNgModuleFactory
8+
} from '@angular/core';
29
import {MatSnackBar} from '@angular/material/snack-bar';
310

411
import {EXAMPLE_COMPONENTS, LiveExample} from '@angular/components-examples';
512
import {CopierService} from '../copier/copier.service';
13+
import {CodeSnippet} from './code-snippet';
14+
15+
export type Views = 'compact' | 'full' | 'collapsed';
616

717
/** Regular expression that matches a file name and its extension */
818
const fileExtensionRegex = /(.*)\.(\w+)/;
@@ -13,21 +23,26 @@ const fileExtensionRegex = /(.*)\.(\w+)/;
1323
styleUrls: ['./example-viewer.scss']
1424
})
1525
export class ExampleViewer {
26+
@ViewChildren(CodeSnippet) readonly snippet: QueryList<CodeSnippet>;
27+
1628
/** Map of example files that should be displayed in the view-source tab. */
1729
exampleTabs: {[tabName: string]: string};
1830

1931
/** Data for the currently selected example. */
2032
exampleData: LiveExample;
2133

22-
/** Whether the source for the example is being displayed. */
23-
showSource = false;
24-
2534
/** Component type for the current example. */
2635
_exampleComponentType: Type<any>|null = null;
2736

2837
/** Module factory that declares the example component. */
2938
_exampleModuleFactory: NgModuleFactory<any>|null = null;
3039

40+
/** View of the example component. */
41+
@Input() view: Views;
42+
43+
/** Whether to show toggle for compact view. */
44+
@Input() showCompactToggle = false;
45+
3146
/** String key of the currently displayed example. */
3247
@Input()
3348
get example() { return this._example; }
@@ -43,10 +58,21 @@ export class ExampleViewer {
4358
}
4459
private _example: string;
4560

46-
constructor(private snackbar: MatSnackBar, private copier: CopierService) {}
61+
/** Range of lines of the source code to display in compact view. */
62+
@Input() lines: readonly [number, number];
63+
64+
/** Name of file to display in compact view. */
65+
@Input() file: string;
66+
67+
constructor(private readonly snackbar: MatSnackBar, private readonly copier: CopierService) {
68+
}
69+
70+
toggleCompactView() {
71+
this.view === 'compact' ? this.view = 'full' : this.view = 'compact';
72+
}
4773

4874
toggleSourceView(): void {
49-
this.showSource = !this.showSource;
75+
this.view === 'full' ? this.view = 'collapsed' : this.view = 'full';
5076
}
5177

5278
copySource(text: string) {

0 commit comments

Comments
 (0)
Please sign in to comment.