feat(aio): add LiveExampleComponent (#15544)

This commit is contained in:
Ward Bell 2017-03-28 14:41:50 -07:00 committed by Victor Berchet
parent aa16ccda79
commit 861953c95c
7 changed files with 495 additions and 13 deletions

View File

@ -4,9 +4,11 @@
<p>No linenums at code-tabs level</p>
<code-tabs >
<code-pane title='TS code file' language='ts'>class {
foo(param: string) {}
}</code-pane>
<code-pane title='TS code file' language='ts'>
class {
foo(param: string) {}
}
</code-pane>
<code-pane title='HTML content file' language='html'>&lt;h1&gt;Heading&lt;/h1&gt;</code-pane>
<code-pane title='JSON data file' language='json' class='is-anti-pattern'>{ "key": "value" }</code-pane>
</code-tabs>
@ -14,9 +16,11 @@
<p>linenums=true at code-tabs level</p>
<code-tabs linenums='true'>
<code-pane title='TS code file' language='ts'>class {
foo(param: string) {}
}</code-pane>
<code-pane title='TS code file' language='ts'>
class {
foo(param: string) {}
}
</code-pane>
<code-pane title='HTML content file' language='html'>&lt;h1&gt;Heading&lt;/h1&gt;</code-pane>
<code-pane title='JSON data file' language='json' class='is-anti-pattern'>{ "key": "value" }</code-pane>
</code-tabs>
@ -24,9 +28,11 @@
<p>No linenums at code-tabs level; linenums=true for HTML pane</p>
<code-tabs >
<code-pane title='TS code file' language='ts'>class {
foo(param: string) {}
}</code-pane>
<code-pane title='TS code file' language='ts'>
class {
foo(param: string) {}
}
</code-pane>
<code-pane title='HTML content file' language='html' linenums='true'>&lt;h1&gt;Heading&lt;/h1&gt;</code-pane>
<code-pane title='JSON data file' language='json' class='is-anti-pattern'>{ "key": "value" }</code-pane>
</code-tabs>
@ -77,4 +83,27 @@
&lt;/hero-details&gt;
</code-example>
<p>More text follows ...</p>
<h2>&lt;live-example&gt;</h2>
<p>Plain live-example</p>
Try this <live-example></live-example>.
<p>live-example with title atty</p>
<live-example title="Good Example"></live-example>
<p>live-example with title body</p>
<live-example title="Good Example">Try this great example</live-example>
<p>live-example with name</p>
<live-example name="testy" title="Better Example"></live-example>
<p>live-example with spacey name and plnkr</p>
<live-example name=" testy " plnkr="super-plnkr"></live-example>
<p>live-example with name and plnkr but no download</p>
<live-example name="testy" plnkr="super-plnkr" noDownload></live-example>
<p>live-example embedded with name and plnkr</p>
<live-example embedded name="testy" plnkr="super-plnkr"></live-example>
<p>More text follows ...</p>

View File

@ -17,12 +17,13 @@ import { ApiListComponent } from './api/api-list.component';
import { CodeExampleComponent } from './code/code-example.component';
import { CodeTabsComponent } from './code/code-tabs.component';
import { DocTitleComponent } from './doc-title.component';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
/** Components that can be embedded in docs
* such as CodeExampleComponent, LiveExampleComponent,...
*/
export const embeddedComponents: any[] = [
ApiListComponent, CodeExampleComponent, DocTitleComponent, CodeTabsComponent
ApiListComponent, CodeExampleComponent, DocTitleComponent, CodeTabsComponent, LiveExampleComponent
];
/** Injectable class w/ property returning components that can be embedded in docs */
@ -34,7 +35,8 @@ export class EmbeddedComponents {
imports: [ CommonModule, MdTabsModule ],
declarations: [
embeddedComponents,
CodeComponent
CodeComponent,
EmbeddedPlunkerComponent
],
providers: [
EmbeddedComponents,

View File

@ -0,0 +1,15 @@
<span *ngIf="!isEmbedded">
<a [href]="plnkr" target="_blank" title="{{title}}">{{title}}</a>
<span *ngIf="enableDownload">
/ <a [href]="zip" download title="Download example">download example</a>
</span>
</span>
<div *ngIf="isEmbedded">
<div *ngIf="showEmbedded" title="{{title}}">
<aio-embedded-plunker [src]="plnkr"></aio-embedded-plunker>
</div>
<img *ngIf="!showEmbedded" (click)="toggleEmbedded()" [src]="plnkrImg" width="100%" height="100%" alt="{{title}}">
<p *ngIf="enableDownload">
You can also <a [href]="zip" download title="Download example">download this example</a>.
</p>
</div>

View File

@ -0,0 +1,265 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement, ElementRef } from '@angular/core';
import { Location } from '@angular/common';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example.component';
const defaultTestPath = '/test';
describe('LiveExampleComponent', () => {
let hostComponent: HostComponent;
let liveExampleDe: DebugElement;
let liveExampleComponent: LiveExampleComponent;
let fixture: ComponentFixture<HostComponent>;
let testPath: string;
let liveExampleContent: string;
//////// test helpers ////////
@Component({
selector: 'aio-host-comp',
template: `<live-example></live-example>`
})
class HostComponent { }
class TestLocation {
path() { return testPath; }
}
function setHostTemplate(template: string) {
TestBed.overrideComponent(HostComponent, {set: {template}});
}
function testComponent(testFn: () => void) {
return TestBed
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(HostComponent);
hostComponent = fixture.componentInstance;
liveExampleDe = fixture.debugElement.children[0];
liveExampleComponent = liveExampleDe.componentInstance;
// Copy the LiveExample's innerHTML (content)
// into the `liveExampleContent` property as the DocViewer does
liveExampleDe.nativeElement.liveExampleContent = liveExampleContent;
fixture.detectChanges();
})
.then(testFn);
}
//////// tests ////////
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HostComponent, LiveExampleComponent, EmbeddedPlunkerComponent ],
providers: [ {provide: Location, useClass: TestLocation }]
})
// Disable the <iframe> within the EmbeddedPlunkerComponent
.overrideComponent(EmbeddedPlunkerComponent, {set: {template: 'NO IFRAME'}});
testPath = defaultTestPath;
liveExampleContent = undefined;
});
describe('when not embedded', () => {
function getAnchors() {
return liveExampleDe.queryAll(By.css('a')).map(de => de.nativeElement as HTMLAnchorElement);
}
function getHrefs() { return getAnchors().map(a => a.href); }
function getLiveExampleAnchor() { return getAnchors()[0]; }
function getDownloadAnchor() { return getAnchors()[1]; }
it('should create LiveExampleComponent', async(() => {
testComponent(() => {
expect(liveExampleComponent).toBeTruthy('LiveExampleComponent');
});
}));
it('should have expected plunker & download hrefs', async(() => {
testPath = '/tutorial/toh-pt1';
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/eplnkr.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
}));
it('should have expected plunker & download hrefs even when path has # frag', async(() => {
testPath = '/tutorial/toh-pt1#somewhere';
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/eplnkr.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
}));
it('should have expected plunker & download hrefs even when path has ? params', async(() => {
testPath = '/tutorial/toh-pt1?foo=1&bar="bar"';
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-pt1/eplnkr.html');
expect(hrefs[1]).toContain('/toh-pt1/toh-pt1.zip');
});
}));
it('should have expected flat-style plunker when has `flat-style`', async(() => {
testPath = '/tutorial/toh-pt1';
setHostTemplate('<live-example flat-style></live-example>');
testComponent(() => {
// The file should be "plnkr.html", not "eplnkr.html"
expect(getLiveExampleAnchor().href).toContain('/plnkr.html');
});
}));
it('should have expected plunker & download hrefs when has example directory (name)', async(() => {
testPath = '/guide/somewhere';
setHostTemplate('<live-example name="toh-1"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/toh-1/eplnkr.html');
expect(hrefs[1]).toContain('/toh-1/toh-1.zip');
});
}));
it('should have expected plunker & download hrefs when has `plnkr`', async(() => {
testPath = '/testing';
setHostTemplate('<live-example plnkr="app-specs"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/testing/app-specs.eplnkr.html');
expect(hrefs[1]).toContain('/testing/app-specs.testing.zip');
});
}));
it('should have expected plunker & download hrefs when has `name` & `plnkr`', async(() => {
testPath = '/guide/somewhere';
setHostTemplate('<live-example name="testing" plnkr="app-specs"></live-example>');
testComponent(() => {
const hrefs = getHrefs();
expect(hrefs[0]).toContain('/testing/app-specs.eplnkr.html');
expect(hrefs[1]).toContain('/testing/app-specs.testing.zip');
});
}));
it('should not have a download link when `noDownload` atty present', async(() => {
setHostTemplate('<live-example noDownload></live-example>');
testComponent(() => {
expect(getAnchors().length).toBe(1, 'only the live-example anchor');
});
}));
it('should have default title when no title attribute or content', async(() => {
setHostTemplate('<live-example></live-example>');
testComponent(() => {
const expectedTitle = 'live example';
const anchor = getLiveExampleAnchor();
expect(anchor.innerText).toBe(expectedTitle, 'anchor content');
expect(anchor.getAttribute('title')).toBe(expectedTitle, 'title');
});
}));
it('should add title when set `title` attribute', async(() => {
const expectedTitle = 'Great Example';
setHostTemplate(`<live-example title="${expectedTitle}"></live-example>`);
testComponent(() => {
const anchor = getLiveExampleAnchor();
expect(anchor.innerText).toBe(expectedTitle, 'anchor content');
expect(anchor.getAttribute('title')).toBe(expectedTitle, 'title');
});
}));
it('should add title from <live-example> body', async(() => {
liveExampleContent = 'The Greatest Example';
setHostTemplate('<live-example title="ignore this title"></live-example>');
testComponent(() => {
const anchor = getLiveExampleAnchor();
expect(anchor.innerText).toBe(liveExampleContent, 'anchor content');
expect(anchor.getAttribute('title')).toBe(liveExampleContent, 'title');
});
}));
});
describe('when embedded', () => {
function getDownloadAnchor() {
const anchor = liveExampleDe.query(By.css('p > a'));
return anchor && anchor.nativeElement as HTMLAnchorElement;
}
function getEmbeddedPlunkerComponent() {
const compDe = liveExampleDe.query(By.directive(EmbeddedPlunkerComponent));
return compDe && compDe.componentInstance as EmbeddedPlunkerComponent;
}
function getImg() {
const img = liveExampleDe.query(By.css('img'));
return img && img.nativeElement as HTMLImageElement;
}
describe('before click', () => {
it('should have hidden, embedded plunker', async(() => {
setHostTemplate('<live-example embedded></live-example>');
testComponent(() => {
expect(liveExampleComponent.isEmbedded).toBe(true, 'component.isEmbedded');
expect(liveExampleComponent.showEmbedded).toBe(false, 'component.showEmbedded');
expect(getEmbeddedPlunkerComponent()).toBeNull('no EmbeddedPlunkerComponent');
});
}));
it('should have default plunker placeholder image', async(() => {
setHostTemplate('<live-example embedded></live-example>');
testComponent(() => {
expect(getImg().src).toContain('plunker/placeholder.png');
});
}));
it('should have specified plunker placeholder image', async(() => {
const expectedSrc = 'example/demo.png';
setHostTemplate(`<live-example embedded img="${expectedSrc}"></live-example>`);
testComponent(() => {
expect(getImg().src).toContain(expectedSrc);
});
}));
it('should have download paragraph with expected anchor href', async(() => {
testPath = '/tutorial/toh-pt1';
setHostTemplate('<live-example embedded></live-example>');
testComponent(() => {
expect(getDownloadAnchor().href).toContain('/toh-pt1/toh-pt1.zip');
});
}));
it('should not have download paragraph when has `nodownload`', async(() => {
testPath = '/tutorial/toh-pt1';
setHostTemplate('<live-example embedded nodownload></live-example>');
testComponent(() => {
expect(getDownloadAnchor()).toBeNull();
});
}));
});
describe('after click', () => {
function clickImg() {
getImg().click();
fixture.detectChanges();
}
it('should show plunker in the page', async(() => {
setHostTemplate('<live-example embedded></live-example>');
testComponent(() => {
clickImg();
expect(liveExampleComponent.isEmbedded).toBe(true, 'component.isEmbedded');
expect(liveExampleComponent.showEmbedded).toBe(true, 'component.showEmbedded');
expect(getEmbeddedPlunkerComponent()).toBeDefined('has EmbeddedPlunkerComponent');
});
}));
});
});
});

View File

@ -0,0 +1,166 @@
/* tslint:disable component-selector */
import { Component, ElementRef, Input, OnInit, AfterViewInit, ViewChild } from '@angular/core';
import { Location } from '@angular/common';
const defaultPlnkrImg = 'plunker/placeholder.png';
const imageBase = 'assets/images/';
const liveExampleBase = 'content/live-examples/';
const zipBase = 'content/zips/';
/**
* Angular.io Live Example Embedded Component
*
* Renders a link to a live/host example of the doc page.
*
* All attributes and the text content are optional
*
* Usage:
* <live-example
* [name="..."] // name of the example directory
* [plnkr="...""] // name of the plunker file (becomes part of zip file name as well)
* [embedded] // embed the plunker in the doc page, else display in new browser tab (external)
* [img="..."] // image to display if embedded in doc page
* [embedded-style] // show external plnkr in embedded style
* [flat-style] // show external plnkr in flat (original) style
* [noDownload] // no downloadable zip option
* [title="..."]> // text for live example link and tooltip
* text // higher precedence way to specify text for live example link and tooltip
* </live-example>
* Example:
* <p>Run <live-example>Try the live example</live-example></p>.
* // ~/resources/live-examples/{page}/plnkr.html
*
* <p>Run <live-example name="toh-1">this example</live-example></p>.
* // ~/resources/live-examples/toh-1/plnkr.html
*
* // Link to the default plunker in the toh-1 sample
* // The title overrides default ("live example") with "Tour of Heroes - Part 1"
* <p>Run <live-example name="toh-1" title="Tour of Heroes - Part 1"></live-example></p>.
* // ~/resources/live-examples/toh-1/plnkr.html
*
* <p>Run <live-example plnkr="minimal"></live-example></p>.
* // ~/resources/live-examples/{page}/minimal.plnkr.html
*
* // Embed the current page's default plunker
* // Text within tag is "live example"
* // No title (no tooltip)
* <live-example embedded title=""></live-example>
* // ~/resources/live-examples/{page}/eplnkr.html
*
* // Links to a *new* browser tab as an embedded style plunker editor
* <live-example embedded-style>this example</live-example>
* // ~/resources/live-examples/{page}/eplnkr.html
*
* // Links to a *new* browser tab in the flat (original editor) style plunker editor
* <live-example flat-style>this example</live-example>
* // ~/resources/live-examples/{page}/plnkr.html
*
* // Displays within the document page as an embedded style plunker editor
* <live-example name="toh-1" embedded plnkr="minimal" img="toh>Tour of Heroes - Part 1</live-example>
* // ~/resources/live-examples/toh-1/minimal.eplnkr.html
*/
@Component({
selector: 'live-example',
templateUrl: 'live-example.component.html'
})
export class LiveExampleComponent implements OnInit {
enableDownload = true;
isEmbedded = false;
plnkr: string;
plnkrImg: string;
showEmbedded = false;
title: string;
zip: string;
constructor(private elementRef: ElementRef, location: Location ) {
const attrs = this.getAttrs();
let exampleDir = attrs.name;
if (!exampleDir) {
// take last segment, excluding hash fragment and query params
exampleDir = location.path(false).match(/[^\/?\#]+(?=\/?(?:$|\#|\?))/)[0];
}
exampleDir = exampleDir.trim();
let plnkrStyle = 'eplnkr';
this.isEmbedded = boolFromAtty(attrs.embedded);
if (!this.isEmbedded) {
// Not embedded in doc page; determine if is embedded- or flat-style in another browser tab.
// External plunker is embedded style by default.
// Make flat style with `flat-style` or `embedded-style="false`
// Must support aliases
const flatStyle = getAttrValue(['flat-style', 'flatstyle', 'flatStyle']);
const isFlatStyle = boolFromAtty(flatStyle);
const embeddedStyle = getAttrValue(['embedded-style', 'embeddedstyle', 'embeddedStyle']);
const isEmbeddedStyle = boolFromAtty(embeddedStyle, !isFlatStyle);
plnkrStyle = isEmbeddedStyle ? 'eplnkr' : 'plnkr';
}
const plnkrName = attrs.plnkr ? attrs.plnkr.trim() + '.' : '';
this.plnkr = `${liveExampleBase}${exampleDir}/${plnkrName}${plnkrStyle}.html`;
this.zip = `${zipBase}${exampleDir}/${plnkrName}${exampleDir}.zip`;
const noDownload = getAttrValue(['noDownload', 'nodownload']); // noDownload aliases
this.enableDownload = !boolFromAtty(noDownload);
this.plnkrImg = imageBase + (attrs.img || defaultPlnkrImg);
this.title = attrs.title || '';
function getAttrValue(atty: string | string[]) {
return attrs[typeof atty === 'string' ? atty : atty.find(a => attrs[a] !== undefined)];
}
}
getAttrs(): any {
const attrs = this.elementRef.nativeElement.attributes;
const attrMap = {};
Object.keys(attrs).forEach(key => attrMap[attrs[key].name] = attrs[key].value);
return attrMap;
}
ngOnInit() {
// The `liveExampleContent` property is set by the DocViewer when it builds this component.
// It is the original innerHTML of the host element.
// Angular will sanitize this title when displayed so should be plain text.
const title = this.elementRef.nativeElement.liveExampleContent;
this.title = (title || this.title || 'live example').trim();
}
toggleEmbedded () { this.showEmbedded = !this.showEmbedded; }
}
function boolFromAtty(atty: string , def: boolean = false) {
// tslint:disable-next-line:triple-equals
return atty == undefined ? def : atty.trim() !== 'false';
}
///// EmbeddedPlunkerComponent ///
/**
* Hides the <iframe> so we can test LiveExampleComponent without actually triggering
* a call to plunker to load the iframe
*/
@Component({
selector: 'aio-embedded-plunker',
template: `<iframe #iframe frameborder="0" width="100%" height="100%"></iframe>`,
styles: [ 'iframe { min-height: 400px; }']
})
export class EmbeddedPlunkerComponent implements AfterViewInit {
@Input() src: string;
@ViewChild('iframe') iframe: ElementRef;
ngAfterViewInit() {
// DEVELOPMENT TESTING ONLY
// this.src = 'https://angular.io/resources/live-examples/quickstart/ts/eplnkr.html';
if (this.iframe) {
// security: the `src` is always authored by the documentation team
// and is considered to be safe
this.iframe.nativeElement.src = this.src;
}
}
}

View File

@ -90,8 +90,13 @@ export class LocationService {
return true;
}
// check for external link
// don't navigate if external link or zip
const { pathname, search, hash } = anchor;
if (anchor.getAttribute('download') != null) {
return true; // let the download happen
}
const relativeUrl = pathname + search + hash;
this.urlParser.href = relativeUrl;
if (anchor.href !== this.urlParser.href) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB