Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit 592d8ec

Browse files
tinayuangaojelbourn
authored andcommitted
Add initial version of openPlunker (#26)
* Add initial version of openPlunker * Add PlunkerData * Make openPlunker work * Move plunker templates location and example data name * Move plunker template to assets/plunker * Add test * fix lint * Make plunker integrated
1 parent 7391448 commit 592d8ec

15 files changed

+549
-4
lines changed

src/app/examples/example-data.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Example data
3+
* with information about Component name, selector, files used in example, and path to examples
4+
*/
5+
export class ExampleData {
6+
// TODO: figure out how do we get these variables.
7+
description: string = 'Some description for material';
8+
// TODO: use real example and delete the example/ folder.
9+
examplePath = '/assets/example/';
10+
exampleFiles = ['button-demo.html', 'button-demo.scss', 'button-demo.ts'];
11+
12+
// TODO: extract these variables from example code.
13+
selectorName = 'button-demo';
14+
indexFilename = 'button-demo';
15+
componentName = 'ButtonDemo';
16+
17+
constructor(example: string) {
18+
if (example) {
19+
this.examplePath = `/app/examples/${example}/`;
20+
// TODO(tinayuangao): Do not hard-code extensions
21+
this.exampleFiles = ['html', 'ts', 'scss']
22+
.map((extension) => `${example}-example.${extension}`);
23+
this.selectorName = this.indexFilename = `${example}-example`;
24+
var exampleName = example.replace(/(?:^\w|\b\w)/g, function(letter) {
25+
return letter.toUpperCase();
26+
});
27+
this.description = exampleName.replace(/[\-]+/g, ' ') + ' Example';
28+
this.componentName = exampleName.replace(/[\-]+/g, '') + 'Example';
29+
}
30+
}
31+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<div class="docs-example-viewer-title-spacer"></div>
55

66
<button md-icon-button type="button" (click)="toggleSourceView()">S</button>
7-
<button md-icon-button type="button">P</button>
7+
<plunker-button [example]="example"></plunker-button>
88
</div>
99

1010
<div class="docs-example-viewer-source" *ngIf="showSource">

src/app/shared/example-viewer/example-viewer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ export class ExampleViewer {
4343
}
4444

4545
exampleFileUrl(extension: string) {
46-
return `/assets/examples/${this.example}-example.${extension.toLowerCase()}.html`;
46+
return `/app/examples/${this.example}/${this.example}-example.${extension.toLowerCase()}`;
4747
}
4848
}

src/app/shared/plunker/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './plunker-button';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- TODO: change the template to be plunker icon -->
2+
<button md-icon-button type="button"(click)="openPlunker()">P</button>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, Input } from '@angular/core';
2+
import { PlunkerWriter } from './plunker-writer';
3+
import { ExampleData } from '../../examples/example-data';
4+
5+
@Component({
6+
selector: 'plunker-button',
7+
templateUrl: './plunker-button.html',
8+
providers: [PlunkerWriter],
9+
})
10+
export class PlunkerButton {
11+
exampleData: ExampleData;
12+
13+
@Input()
14+
set example(example: string) {
15+
this.exampleData = new ExampleData(example);
16+
}
17+
18+
constructor(private plunkerWriter: PlunkerWriter) {}
19+
20+
openPlunker(): void {
21+
this.plunkerWriter.openPlunker(this.exampleData);
22+
}
23+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {TestBed, inject, async, flushMicrotasks, fakeAsync} from '@angular/core/testing';
2+
import {MockBackend} from '@angular/http/testing';
3+
import {BaseRequestOptions, Http, Response, ResponseOptions, XHRBackend} from '@angular/http';
4+
import {PlunkerWriter} from './plunker-writer';
5+
import {ExampleData} from '../../examples/example-data';
6+
7+
8+
describe('PlunkerWriter', () => {
9+
var plunkerWriter: PlunkerWriter;
10+
var data: ExampleData;
11+
beforeEach(async(() => {
12+
TestBed.configureTestingModule({
13+
imports: [],
14+
declarations: [],
15+
providers: [
16+
PlunkerWriter,
17+
MockBackend,
18+
BaseRequestOptions,
19+
{provide: XHRBackend, useExisting: MockBackend},
20+
{
21+
provide: Http,
22+
useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => {
23+
return new Http(backendInstance, defaultOptions);
24+
},
25+
deps: [MockBackend, BaseRequestOptions]
26+
},
27+
]
28+
});
29+
30+
TestBed.compileComponents();
31+
}));
32+
33+
beforeEach(inject([MockBackend], (mockBackend: MockBackend, http: Http) => {
34+
mockBackend.connections.subscribe((connection: any) => {
35+
const url = connection.request.url;
36+
connection.mockRespond(getFakeDocResponse(url));
37+
});
38+
39+
plunkerWriter = TestBed.get(PlunkerWriter);
40+
data = new ExampleData();
41+
data.examplePath = 'http://material.angular.io/';
42+
data.exampleFiles = ['test.ts', 'test.html', 'src/detail.ts'];
43+
}));
44+
45+
it('should append correct copyright', () => {
46+
expect(plunkerWriter._appendCopyright('test.ts', 'NoContent')).toBe(`NoContent
47+
48+
/** Copyright 2016 Google Inc. All Rights Reserved.
49+
Use of this source code is governed by an MIT-style license that
50+
can be found in the LICENSE file at http://angular.io/license */`);
51+
52+
expect(plunkerWriter._appendCopyright('test.html', 'NoContent')).toBe(`NoContent
53+
54+
<!-- Copyright 2016 Google Inc. All Rights Reserved.
55+
Use of this source code is governed by an MIT-style license that
56+
can be found in the LICENSE file at http://angular.io/license -->`);
57+
58+
});
59+
60+
it('should create form element', () => {
61+
expect(plunkerWriter._createFormElement().outerHTML).toBe(
62+
`<form action="https://plnkr.co/edit/?p=preview" method="post" target="_blank"></form>`);
63+
});
64+
65+
it('should add files to form input', () => {
66+
plunkerWriter.form = plunkerWriter._createFormElement();
67+
68+
plunkerWriter._addFileToForm('NoContent', 'test.ts', 'path/to/file');
69+
plunkerWriter._addFileToForm('Test', 'test.html', 'path/to/file');
70+
plunkerWriter._addFileToForm('Detail', 'src/detail.ts', 'path/to/file');
71+
72+
let elements = plunkerWriter.form.elements;
73+
expect(elements.length).toBe(3);
74+
expect(elements[0].getAttribute('name')).toBe('files[test.ts]');
75+
expect(elements[1].getAttribute('name')).toBe('files[test.html]');
76+
expect(elements[2].getAttribute('name')).toBe('files[src/detail.ts]');
77+
});
78+
79+
it('should open a new window with plunker url', fakeAsync(() => {
80+
plunkerWriter.openPlunker(data);
81+
flushMicrotasks();
82+
83+
let elements = plunkerWriter.form.elements;
84+
expect(elements.length).toBe(11);
85+
86+
// Should have correct tags
87+
expect(elements[0].getAttribute('name')).toBe('tags[0]');
88+
expect(elements[0].getAttribute('value')).toBe('angular');
89+
expect(elements[1].getAttribute('value')).toBe('material');
90+
expect(elements[2].getAttribute('value')).toBe('example');
91+
92+
// Should have private and description
93+
expect(elements[3].getAttribute('name')).toBe('private');
94+
expect(elements[4].getAttribute('name')).toBe('description');
95+
96+
// Should have example files
97+
expect(elements[5].getAttribute('name')).toBe('files[index.html]');
98+
expect(elements[6].getAttribute('name')).toBe('files[systemjs.config.js]');
99+
expect(elements[7].getAttribute('name')).toBe('files[main.ts]');
100+
101+
// Should have template files
102+
expect(elements[8].getAttribute('name')).toBe('files[test.ts]');
103+
expect(elements[9].getAttribute('name')).toBe('files[test.html]');
104+
expect(elements[10].getAttribute('name')).toBe('files[src/detail.ts]');
105+
106+
// TODO(tinagao): Add more test
107+
}));
108+
});
109+
110+
const FAKE_DOCS = {
111+
'http://material.angular.io/test.ts': 'ExampleComponent',
112+
'http://material.angular.io/test.html': `
113+
<example></example>`,
114+
'http://material.angular.io/src/detail.ts': 'DetailComponent',
115+
};
116+
117+
function getFakeDocResponse(url: string) {
118+
if (url in FAKE_DOCS) {
119+
return new Response(new ResponseOptions({
120+
status: 200,
121+
body: FAKE_DOCS[url],
122+
}));
123+
} else {
124+
return new Response(new ResponseOptions({status: 404}));
125+
}
126+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Injectable } from '@angular/core';
2+
import { Http } from '@angular/http';
3+
import { ExampleData } from '../../examples/example-data';
4+
import 'rxjs/add/operator/toPromise';
5+
6+
const PLUNKER_URL = 'https://plnkr.co/edit/?p=preview';
7+
8+
const COPYRIGHT =
9+
`Copyright 2016 Google Inc. All Rights Reserved.
10+
Use of this source code is governed by an MIT-style license that
11+
can be found in the LICENSE file at http://angular.io/license`;
12+
13+
const TEMPLATE_PATH = '/assets/plunker/';
14+
const TEMPLATE_FILES = ['index.html', 'systemjs.config.js', 'main.ts'];
15+
16+
const TAGS: string[] = ['angular', 'material', 'example'];
17+
18+
/**
19+
* Plunker writer, write example files to Plunker
20+
*
21+
* Plunker API
22+
* URL: http://plnkr.co/edit/?p=preview
23+
* data: {
24+
* // File name, directory and content of files
25+
* files[file-name1]: file-content1,
26+
* files[directory-name/file-name2]: file-content2,
27+
* // Can add multiple tags
28+
* tags[0]: tag-0,
29+
* // Description of plunker
30+
* description: description,
31+
* // Private or not
32+
* private: true
33+
* }
34+
*/
35+
@Injectable()
36+
export class PlunkerWriter {
37+
form: HTMLFormElement;
38+
exampleData: ExampleData;
39+
40+
constructor(private _http: Http) {}
41+
42+
/** Construct the plunker content */
43+
openPlunker(data: ExampleData) {
44+
this.exampleData = data;
45+
46+
this.form = this._createFormElement();
47+
48+
for (let i = 0; i < TAGS.length; i++) {
49+
this._createFormInput(`tags[${i}]`, TAGS[i]);
50+
}
51+
52+
this._createFormInput('private', 'true');
53+
this._createFormInput('description', this.exampleData.description);
54+
55+
var templateContents = TEMPLATE_FILES.map((file) => this._readFile(file, TEMPLATE_PATH));
56+
var exampleContents = this.exampleData.exampleFiles.map(
57+
(file) => this._readFile(file, this.exampleData.examplePath));
58+
59+
Promise.all(templateContents.concat(exampleContents)).then((_) => this.form.submit());
60+
}
61+
62+
_createFormElement(): HTMLFormElement {
63+
var form = document.createElement('form');
64+
form.action = PLUNKER_URL;
65+
form.method = 'post';
66+
form.target = '_blank';
67+
return form;
68+
}
69+
70+
_createFormInput(name: string, value: string) {
71+
var input = document.createElement('input');
72+
input.type = 'hidden';
73+
input.name = name;
74+
input.value = value;
75+
this.form.appendChild(input);
76+
}
77+
78+
_readFile(filename: string, path: string) {
79+
return this._http.get(path + filename).toPromise().then(
80+
response => this._addFileToForm(response.text(), filename, path),
81+
error => console.log(error));
82+
}
83+
84+
_addFileToForm(content: string, filename: string, path: string) {
85+
if (path == TEMPLATE_PATH) {
86+
content = this._replaceExamplePlaceholderNames(filename, content);
87+
}
88+
this._createFormInput(`files[${filename}]`, this._appendCopyright(filename, content));
89+
}
90+
91+
/**
92+
* The Plunker template assets contain placeholder names for the examples:
93+
* "<material-docs-example>" and "MaterialDocsExample".
94+
* This will replace those placeholders with the names from the example metadata,
95+
* e.g. "<basic-button-example>" and "BasicButtonExample"
96+
*/
97+
_replaceExamplePlaceholderNames(fileName: string, fileContent: string): string {
98+
if (fileName == 'index.html') {
99+
// Replace the component selector in `index,html`.
100+
// For example, <material-docs-example></material-docs-exmaple> will be replaced as
101+
// <button-demo></button-demo>
102+
fileContent = fileContent.replace(/material-docs-example/g, this.exampleData.selectorName);
103+
} else if (fileName == 'main.ts') {
104+
// Replace the component name in `main.ts`.
105+
// For example, `import {MaterialDocsExample} from 'material-docs-example'`
106+
// will be replaced as `import {ButtonDemo} from './button-demo'`
107+
fileContent = fileContent.replace(/MaterialDocsExample/g, this.exampleData.componentName);
108+
fileContent = fileContent.replace(/material-docs-example/g, this.exampleData.indexFilename);
109+
}
110+
return fileContent;
111+
}
112+
113+
_appendCopyright(filename: string, content: string) {
114+
if (filename.indexOf('.ts') > -1 || filename.indexOf('.scss') > -1) {
115+
content = `${content}\n\n/** ${COPYRIGHT} */`;
116+
} else if (filename.indexOf('.html') > -1) {
117+
content = `${content}\n\n<!-- ${COPYRIGHT} -->`;
118+
}
119+
return content;
120+
}
121+
}

src/app/shared/shared-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {NavBar} from './navbar/navbar';
77
import {MaterialModule} from '@angular/material';
88
import {BrowserModule} from '@angular/platform-browser';
99
import {RouterModule} from '@angular/router';
10+
import {PlunkerButton} from './plunker';
1011

1112

1213
@NgModule({
@@ -16,8 +17,8 @@ import {RouterModule} from '@angular/router';
1617
BrowserModule,
1718
MaterialModule,
1819
],
19-
declarations: [DocViewer, ExampleViewer, NavBar],
20-
exports: [DocViewer, ExampleViewer, NavBar],
20+
declarations: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
21+
exports: [DocViewer, ExampleViewer, NavBar, PlunkerButton],
2122
providers: [DocumentationItems],
2223
entryComponents: [
2324
ExampleViewer,

0 commit comments

Comments
 (0)