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

perf: example rendering performance improvements #961

Merged
merged 1 commit into from
Apr 27, 2021
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
36 changes: 19 additions & 17 deletions src/app/shared/stack-blitz/stack-blitz-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {StackBlitzWriter} from './stack-blitz-writer';
@Component({
selector: 'stack-blitz-button',
templateUrl: './stack-blitz-button.html',
providers: [StackBlitzWriter],
})
export class StackBlitzButton {
/**
Expand All @@ -18,28 +17,31 @@ export class StackBlitzButton {
* StackBlitz not yet being ready for people with poor network connections or slow devices.
*/
isDisabled = false;
stackBlitzForm: HTMLFormElement | undefined;
exampleData: ExampleData | undefined;

@HostListener('mouseover') onMouseOver() {
this.isDisabled = !this.stackBlitzForm;
/**
* Form used to submit the data to Stackblitz.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Form used to submit the data to Stackblitz.
* Form used to submit the data to StackBlitz.

* Important! it needs to be constructed ahead-of-time, because doing so on-demand
* will cause Firefox to block the submit as a popup, because it didn't happen within
* the same tick as the user interaction.
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Important! it needs to be constructed ahead-of-time, because doing so on-demand
* will cause Firefox to block the submit as a popup, because it didn't happen within
* the same tick as the user interaction.
* Important! It needs to be constructed ahead-of-time, because doing so on-demand
* would cause Firefox to block the submission as a popup. This is because it would't
* happen within the same tick as the user interaction.

*/
private _stackBlitzForm: HTMLFormElement | undefined;

@HostListener('mouseover')
onMouseOver() {
this.isDisabled = !this._stackBlitzForm;
}

@Input()
set example(example: string | undefined) {
if (example) {
const isTest = example.includes('harness');
this.exampleData = new ExampleData(example);
if (this.exampleData) {
this.stackBlitzWriter.constructStackBlitzForm(example,
this.exampleData,
example.includes('harness'))
.then((stackBlitzForm: HTMLFormElement) => {
this.stackBlitzForm = stackBlitzForm;
this.stackBlitzWriter.constructStackBlitzForm(example, this.exampleData, isTest)
.then(form => {
this._stackBlitzForm = form;
this.isDisabled = false;
});
} else {
this.isDisabled = true;
}
} else {
this.isDisabled = true;
}
Expand All @@ -52,10 +54,10 @@ export class StackBlitzButton {
// to submit if it is detached from the document. See the following chromium commit for
// more details:
// https://chromium.googlesource.com/chromium/src/+/962c2a22ddc474255c776aefc7abeba00edc7470%5E!
if (this.stackBlitzForm) {
document.body.appendChild(this.stackBlitzForm);
this.stackBlitzForm.submit();
document.body.removeChild(this.stackBlitzForm);
if (this._stackBlitzForm) {
document.body.appendChild(this._stackBlitzForm);
this._stackBlitzForm.submit();
document.body.removeChild(this._stackBlitzForm);
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/app/shared/stack-blitz/stack-blitz-writer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ describe('StackBlitzWriter', () => {
});

it('should append correct copyright', () => {
const year = new Date().getFullYear();
expect(stackBlitzWriter._appendCopyright('test.ts', 'NoContent')).toBe(`NoContent

/** Copyright 2020 Google LLC. All Rights Reserved.
/** Copyright ${year} 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 http://angular.io/license */`);

expect(stackBlitzWriter._appendCopyright('test.html', 'NoContent')).toBe(`NoContent

<!-- Copyright 2020 Google LLC. All Rights Reserved.
<!-- Copyright ${year} 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 http://angular.io/license -->`);

Expand Down
74 changes: 43 additions & 31 deletions src/app/shared/stack-blitz/stack-blitz-writer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Injectable, NgZone} from '@angular/core';
import {VERSION} from '@angular/material/core';
import {EXAMPLE_COMPONENTS, ExampleData} from '@angular/components-examples';
import {Observable} from 'rxjs';
import {shareReplay, take} from 'rxjs/operators';

import {materialVersion} from '../version/version';

const STACKBLITZ_URL = 'https://run.stackblitz.com/api/angular/v1';

const COPYRIGHT =
`Copyright 2020 Google LLC. All Rights Reserved.
`Copyright ${new Date().getFullYear()} Google LLC. All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

👏🏼

Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license`;

Expand Down Expand Up @@ -116,17 +118,18 @@ const testDependencies = {
* dependencies: dependencies
* }
*/
@Injectable()
@Injectable({providedIn: 'root'})
export class StackBlitzWriter {
constructor(private _http: HttpClient) {}
private _fileCache = new Map<string, Observable<string>>();

constructor(private _http: HttpClient, private _ngZone: NgZone) {}

/**
* Returns an HTMLFormElement that will open a new StackBlitz template with the example data when
* called with submit().
*/
constructStackBlitzForm(exampleId: string,
data: ExampleData,
isTest: boolean): Promise<HTMLFormElement> {
async constructStackBlitzForm(exampleId: string, data: ExampleData,
isTest: boolean): Promise<HTMLFormElement> {
const liveExample = EXAMPLE_COMPONENTS[exampleId];
const indexFile = `src%2Fapp%2F${data.indexFilename}`;
const form = this._createFormElement(indexFile);
Expand All @@ -140,26 +143,30 @@ export class StackBlitzWriter {
'dependencies',
JSON.stringify(isTest ? testDependencies : dependencies));

return new Promise(resolve => {
const templateContents = (isTest ? TEST_TEMPLATE_FILES : TEMPLATE_FILES)
.map(file => this._readFile(form,
data,
file,
isTest ? TEST_TEMPLATE_PATH : TEMPLATE_PATH,
isTest));
// Run outside the zone since this form doesn't interact with Angular
// and the file requests can cause excessive change detections.
await this._ngZone.runOutsideAngular(() => {
const fileReadPromises: Promise<void>[] = [];

// Read all of the template files.
(isTest ? TEST_TEMPLATE_FILES : TEMPLATE_FILES).forEach(file => fileReadPromises.push(
this._loadAndAppendFile(form, data, file, isTest ? TEST_TEMPLATE_PATH : TEMPLATE_PATH,
isTest)));

const exampleContents = data.exampleFiles
.map(file => this._readFile(form, data, file, baseExamplePath, isTest));
// Read the example-specific files.
data.exampleFiles.forEach(file => fileReadPromises.push(this._loadAndAppendFile(form, data,
file, baseExamplePath, isTest)));

// TODO(josephperrott): Prevent including assets to be manually checked.
if (data.selectorName === 'icon-svg-example') {
this._readFile(form, data, 'assets/img/examples/thumbup-icon.svg', '', isTest, false);
fileReadPromises.push(this._loadAndAppendFile(form, data,
'assets/img/examples/thumbup-icon.svg', '', isTest, false));
}

Promise.all(templateContents.concat(exampleContents)).then(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Fun fact: this Promise.all didn't actually do anything, because the passed-in array was filled with undefined values.

Copy link
Contributor

Choose a reason for hiding this comment

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

😬

resolve(form);
});
return Promise.all(fileReadPromises);
});

return form;
}

/** Constructs a new form element that will navigate to the StackBlitz url. */
Expand All @@ -172,7 +179,7 @@ export class StackBlitzWriter {
}

/** Appends the name and value as an input to the form. */
_appendFormInput(form: HTMLFormElement, name: string, value: string): void {
private _appendFormInput(form: HTMLFormElement, name: string, value: string): void {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
Expand All @@ -189,13 +196,18 @@ export class StackBlitzWriter {
* @param isTest whether file is part of a test example
* @param prependApp whether to prepend the 'app' prefix to the path
*/
_readFile(form: HTMLFormElement,
data: ExampleData,
filename: string,
path: string,
isTest: boolean,
prependApp = true): void {
this._http.get(path + filename, {responseType: 'text'}).subscribe(
private _loadAndAppendFile(form: HTMLFormElement, data: ExampleData, filename: string,
path: string, isTest: boolean, prependApp = true): Promise<void> {
const url = path + filename;
let stream = this._fileCache.get(url);

if (!stream) {
stream = this._http.get(url, {responseType: 'text'}).pipe(shareReplay(1));
this._fileCache.set(url, stream);
}

// The `take(1)` is necessary, because the Promise from `toPromise` resolves on complete.
return stream.pipe(take(1)).toPromise().then(
response => this._addFileToForm(form, data, response, filename, path, isTest, prependApp),
error => console.log(error)
);
Expand Down Expand Up @@ -232,9 +244,9 @@ export class StackBlitzWriter {
* This will replace those placeholders with the names from the example metadata,
* e.g. "<basic-button-example>" and "BasicButtonExample"
*/
_replaceExamplePlaceholderNames(data: ExampleData,
fileName: string,
fileContent: string): string {
private _replaceExamplePlaceholderNames(data: ExampleData,
fileName: string,
fileContent: string): string {
if (fileName === 'src/index.html') {
// Replace the component selector in `index,html`.
// For example, <material-docs-example></material-docs-example> will be replaced as
Expand Down