1
1
import { HttpClient } from '@angular/common/http' ;
2
- import { Injectable } from '@angular/core' ;
2
+ import { Injectable , NgZone } from '@angular/core' ;
3
3
import { VERSION } from '@angular/material/core' ;
4
4
import { EXAMPLE_COMPONENTS , ExampleData } from '@angular/components-examples' ;
5
+ import { Observable } from 'rxjs' ;
6
+ import { shareReplay , take } from 'rxjs/operators' ;
5
7
6
8
import { materialVersion } from '../version/version' ;
7
9
8
10
const STACKBLITZ_URL = 'https://run.stackblitz.com/api/angular/v1' ;
9
11
10
12
const COPYRIGHT =
11
- `Copyright 2020 Google LLC. All Rights Reserved.
13
+ `Copyright ${ new Date ( ) . getFullYear ( ) } Google LLC. All Rights Reserved.
12
14
Use of this source code is governed by an MIT-style license that
13
15
can be found in the LICENSE file at http://angular.io/license` ;
14
16
@@ -116,17 +118,18 @@ const testDependencies = {
116
118
* dependencies: dependencies
117
119
* }
118
120
*/
119
- @Injectable ( )
121
+ @Injectable ( { providedIn : 'root' } )
120
122
export class StackBlitzWriter {
121
- constructor ( private _http : HttpClient ) { }
123
+ private _fileCache = new Map < string , Observable < string > > ( ) ;
124
+
125
+ constructor ( private _http : HttpClient , private _ngZone : NgZone ) { }
122
126
123
127
/**
124
128
* Returns an HTMLFormElement that will open a new StackBlitz template with the example data when
125
129
* called with submit().
126
130
*/
127
- constructStackBlitzForm ( exampleId : string ,
128
- data : ExampleData ,
129
- isTest : boolean ) : Promise < HTMLFormElement > {
131
+ async constructStackBlitzForm ( exampleId : string , data : ExampleData ,
132
+ isTest : boolean ) : Promise < HTMLFormElement > {
130
133
const liveExample = EXAMPLE_COMPONENTS [ exampleId ] ;
131
134
const indexFile = `src%2Fapp%2F${ data . indexFilename } ` ;
132
135
const form = this . _createFormElement ( indexFile ) ;
@@ -140,26 +143,30 @@ export class StackBlitzWriter {
140
143
'dependencies' ,
141
144
JSON . stringify ( isTest ? testDependencies : dependencies ) ) ;
142
145
143
- return new Promise ( resolve => {
144
- const templateContents = ( isTest ? TEST_TEMPLATE_FILES : TEMPLATE_FILES )
145
- . map ( file => this . _readFile ( form ,
146
- data ,
147
- file ,
148
- isTest ? TEST_TEMPLATE_PATH : TEMPLATE_PATH ,
149
- isTest ) ) ;
146
+ // Run outside the zone since this form doesn't interact with Angular
147
+ // and the file requests can cause excessive change detections.
148
+ await this . _ngZone . runOutsideAngular ( ( ) => {
149
+ const fileReadPromises : Promise < void > [ ] = [ ] ;
150
+
151
+ // Read all of the template files.
152
+ ( isTest ? TEST_TEMPLATE_FILES : TEMPLATE_FILES ) . forEach ( file => fileReadPromises . push (
153
+ this . _loadAndAppendFile ( form , data , file , isTest ? TEST_TEMPLATE_PATH : TEMPLATE_PATH ,
154
+ isTest ) ) ) ;
150
155
151
- const exampleContents = data . exampleFiles
152
- . map ( file => this . _readFile ( form , data , file , baseExamplePath , isTest ) ) ;
156
+ // Read the example-specific files.
157
+ data . exampleFiles . forEach ( file => fileReadPromises . push ( this . _loadAndAppendFile ( form , data ,
158
+ file , baseExamplePath , isTest ) ) ) ;
153
159
154
160
// TODO(josephperrott): Prevent including assets to be manually checked.
155
161
if ( data . selectorName === 'icon-svg-example' ) {
156
- this . _readFile ( form , data , 'assets/img/examples/thumbup-icon.svg' , '' , isTest , false ) ;
162
+ fileReadPromises . push ( this . _loadAndAppendFile ( form , data ,
163
+ 'assets/img/examples/thumbup-icon.svg' , '' , isTest , false ) ) ;
157
164
}
158
165
159
- Promise . all ( templateContents . concat ( exampleContents ) ) . then ( ( ) => {
160
- resolve ( form ) ;
161
- } ) ;
166
+ return Promise . all ( fileReadPromises ) ;
162
167
} ) ;
168
+
169
+ return form ;
163
170
}
164
171
165
172
/** Constructs a new form element that will navigate to the StackBlitz url. */
@@ -172,7 +179,7 @@ export class StackBlitzWriter {
172
179
}
173
180
174
181
/** Appends the name and value as an input to the form. */
175
- _appendFormInput ( form : HTMLFormElement , name : string , value : string ) : void {
182
+ private _appendFormInput ( form : HTMLFormElement , name : string , value : string ) : void {
176
183
const input = document . createElement ( 'input' ) ;
177
184
input . type = 'hidden' ;
178
185
input . name = name ;
@@ -189,13 +196,18 @@ export class StackBlitzWriter {
189
196
* @param isTest whether file is part of a test example
190
197
* @param prependApp whether to prepend the 'app' prefix to the path
191
198
*/
192
- _readFile ( form : HTMLFormElement ,
193
- data : ExampleData ,
194
- filename : string ,
195
- path : string ,
196
- isTest : boolean ,
197
- prependApp = true ) : void {
198
- this . _http . get ( path + filename , { responseType : 'text' } ) . subscribe (
199
+ private _loadAndAppendFile ( form : HTMLFormElement , data : ExampleData , filename : string ,
200
+ path : string , isTest : boolean , prependApp = true ) : Promise < void > {
201
+ const url = path + filename ;
202
+ let stream = this . _fileCache . get ( url ) ;
203
+
204
+ if ( ! stream ) {
205
+ stream = this . _http . get ( url , { responseType : 'text' } ) . pipe ( shareReplay ( 1 ) ) ;
206
+ this . _fileCache . set ( url , stream ) ;
207
+ }
208
+
209
+ // The `take(1)` is necessary, because the Promise from `toPromise` resolves on complete.
210
+ return stream . pipe ( take ( 1 ) ) . toPromise ( ) . then (
199
211
response => this . _addFileToForm ( form , data , response , filename , path , isTest , prependApp ) ,
200
212
error => console . log ( error )
201
213
) ;
@@ -232,9 +244,9 @@ export class StackBlitzWriter {
232
244
* This will replace those placeholders with the names from the example metadata,
233
245
* e.g. "<basic-button-example>" and "BasicButtonExample"
234
246
*/
235
- _replaceExamplePlaceholderNames ( data : ExampleData ,
236
- fileName : string ,
237
- fileContent : string ) : string {
247
+ private _replaceExamplePlaceholderNames ( data : ExampleData ,
248
+ fileName : string ,
249
+ fileContent : string ) : string {
238
250
if ( fileName === 'src/index.html' ) {
239
251
// Replace the component selector in `index,html`.
240
252
// For example, <material-docs-example></material-docs-example> will be replaced as
0 commit comments