diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index a4272e9f44..dc43b7c201 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type} from '@angular/core'; +import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type, ɵisPromise} from '@angular/core'; import {ɵTRANSITION_ID} from '@angular/platform-browser'; import {first} from 'rxjs/operators'; @@ -45,12 +45,18 @@ the server-rendered app can be properly bootstrapped into a client app.`); .then(() => { const platformState = platform.injector.get(PlatformState); + const asyncPromises: Promise[] = []; + // Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string. const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null); if (callbacks) { for (const callback of callbacks) { try { - callback(); + const callbackResult = callback(); + if (ɵisPromise(callbackResult)) { + asyncPromises.push(callbackResult); + } + } catch (e) { // Ignore exceptions. console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e); @@ -58,9 +64,22 @@ the server-rendered app can be properly bootstrapped into a client app.`); } } - const output = platformState.renderToString(); - platform.destroy(); - return output; + const complete = () => { + const output = platformState.renderToString(); + platform.destroy(); + return output; + }; + + if (asyncPromises.length === 0) { + return complete(); + } + + return Promise + .all(asyncPromises.map(asyncPromise => { + return asyncPromise.catch( + e => { console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e); }); + })) + .then(complete); }); }); } diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 3bac6084ad..5c364efd85 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -57,6 +57,24 @@ function getMetaRenderHook(doc: any) { }; } +function getAsyncTitleRenderHook(doc: any) { + return () => { + // Async set the title as part of the render hook. + return new Promise(resolve => { + setTimeout(() => { + doc.title = 'AsyncRenderHook'; + resolve(); + }); + }); + }; +} + +function asyncRejectRenderHook() { + return () => { + return new Promise((_resolve, reject) => { setTimeout(() => { reject('reject'); }); }); + }; +} + @NgModule({ bootstrap: [MyServerApp], declarations: [MyServerApp], @@ -81,6 +99,39 @@ class RenderHookModule { class MultiRenderHookModule { } +@NgModule({ + bootstrap: [MyServerApp], + declarations: [MyServerApp], + imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule], + providers: [ + { + provide: BEFORE_APP_SERIALIZED, + useFactory: getAsyncTitleRenderHook, + multi: true, + deps: [DOCUMENT] + }, + ] +}) +class AsyncRenderHookModule { +} +@NgModule({ + bootstrap: [MyServerApp], + declarations: [MyServerApp], + imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule], + providers: [ + {provide: BEFORE_APP_SERIALIZED, useFactory: getMetaRenderHook, multi: true, deps: [DOCUMENT]}, + { + provide: BEFORE_APP_SERIALIZED, + useFactory: getAsyncTitleRenderHook, + multi: true, + deps: [DOCUMENT] + }, + {provide: BEFORE_APP_SERIALIZED, useFactory: asyncRejectRenderHook, multi: true}, + ] +}) +class AsyncMultiRenderHookModule { +} + @Component({selector: 'app', template: `Works too!`}) class MyServerApp2 { } @@ -699,6 +750,28 @@ class HiddenModule { called = true; }); })); + + it('should call async render hooks', async(() => { + renderModule(AsyncRenderHookModule, {document: doc}).then(output => { + // title should be added by the render hook. + expect(output).toBe( + 'AsyncRenderHook' + + 'Works!'); + called = true; + }); + })); + + it('should call multiple async and sync render hooks', async(() => { + const consoleSpy = spyOn(console, 'warn'); + renderModule(AsyncMultiRenderHookModule, {document: doc}).then(output => { + // title should be added by the render hook. + expect(output).toBe( + 'AsyncRenderHook' + + 'Works!'); + expect(consoleSpy).toHaveBeenCalled(); + called = true; + }); + })); }); describe('http', () => {