From 8dfc3c386af17977547500b9fb3b7974991a770a Mon Sep 17 00:00:00 2001 From: Vikram Subramanian Date: Mon, 4 Sep 2017 00:38:42 -0700 Subject: [PATCH] feat(platform-server): provide a way to hook into renderModule* (#19023) A multi RENDER_MODULE_HOOK provider can provide function that will be called with the current document just before the document is rendered to string. This hook can for example be used for the state transfer module to serialize any server state that needs to be transported to the client, just before the current platform state is rendered to string. PR Close #19023 --- .../platform-server/src/platform-server.ts | 2 +- packages/platform-server/src/tokens.ts | 9 +++ packages/platform-server/src/utils.ts | 19 +++++- .../platform-server/test/integration_spec.ts | 68 ++++++++++++++++++- .../platform-server/index.d.ts | 3 + 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/platform-server/src/platform-server.ts b/packages/platform-server/src/platform-server.ts index ffb56751c0..b67f16a5a2 100644 --- a/packages/platform-server/src/platform-server.ts +++ b/packages/platform-server/src/platform-server.ts @@ -8,7 +8,7 @@ export {PlatformState} from './platform_state'; export {ServerModule, platformDynamicServer, platformServer} from './server'; -export {INITIAL_CONFIG, PlatformConfig} from './tokens'; +export {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens'; export {renderModule, renderModuleFactory} from './utils'; export * from './private_export'; diff --git a/packages/platform-server/src/tokens.ts b/packages/platform-server/src/tokens.ts index c212a96668..f1369d6e22 100644 --- a/packages/platform-server/src/tokens.ts +++ b/packages/platform-server/src/tokens.ts @@ -24,3 +24,12 @@ export interface PlatformConfig { * @experimental */ export const INITIAL_CONFIG = new InjectionToken('Server.INITIAL_CONFIG'); + +/** + * A function that will be executed when calling `renderModuleFactory` or `renderModule` just + * before current platform state is rendered to string. + * + * @experimental + */ +export const BEFORE_APP_SERIALIZED = + new InjectionToken void>>('Server.RENDER_MODULE_HOOK'); diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index b26593aa23..1ec9117889 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -14,7 +14,7 @@ import {toPromise} from 'rxjs/operator/toPromise'; import {PlatformState} from './platform_state'; import {platformDynamicServer, platformServer} from './server'; -import {INITIAL_CONFIG} from './tokens'; +import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens'; interface PlatformOptions { document?: string; @@ -45,7 +45,22 @@ the server-rendered app can be properly bootstrapped into a client app.`); return toPromise .call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable))) .then(() => { - const output = platform.injector.get(PlatformState).renderToString(); + const platformState = platform.injector.get(PlatformState); + + // 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(); + } catch (e) { + // Ignore exceptions. + console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e); + } + } + } + + const output = platformState.renderToString(); platform.destroy(); return output; }); diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 2e73010975..b6a2a75d2e 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -16,7 +16,7 @@ import {Http, HttpModule, Response, ResponseOptions, XHRBackend} from '@angular/ import {MockBackend, MockConnection} from '@angular/http/testing'; import {BrowserModule, DOCUMENT, Title} from '@angular/platform-browser'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server'; +import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server'; import {Subscription} from 'rxjs/Subscription'; import {filter} from 'rxjs/operator/filter'; import {first} from 'rxjs/operator/first'; @@ -38,6 +38,50 @@ class MyServerApp { class ExampleModule { } +function getTitleRenderHook(doc: any) { + return () => { + // Set the title as part of the render hook. + doc.title = 'RenderHook'; + }; +} + +function exceptionRenderHook() { + throw new Error('error'); +} + +function getMetaRenderHook(doc: any) { + return () => { + // Add a meta tag before rendering the document. + const metaElement = doc.createElement('meta'); + metaElement.setAttribute('name', 'description'); + doc.head.appendChild(metaElement); + }; +} + +@NgModule({ + bootstrap: [MyServerApp], + declarations: [MyServerApp], + imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule], + providers: [ + {provide: BEFORE_APP_SERIALIZED, useFactory: getTitleRenderHook, multi: true, deps: [DOCUMENT]}, + ] +}) +class RenderHookModule { +} + +@NgModule({ + bootstrap: [MyServerApp], + declarations: [MyServerApp], + imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule], + providers: [ + {provide: BEFORE_APP_SERIALIZED, useFactory: getTitleRenderHook, multi: true, deps: [DOCUMENT]}, + {provide: BEFORE_APP_SERIALIZED, useValue: exceptionRenderHook, multi: true}, + {provide: BEFORE_APP_SERIALIZED, useFactory: getMetaRenderHook, multi: true, deps: [DOCUMENT]}, + ] +}) +class MultiRenderHookModule { +} + @Component({selector: 'app', template: `Works too!`}) class MyServerApp2 { } @@ -469,6 +513,28 @@ export function main() { called = true; }); })); + + it('should call render hook', async(() => { + renderModule(RenderHookModule, {document: doc}).then(output => { + // title should be added by the render hook. + expect(output).toBe( + 'RenderHook' + + 'Works!'); + called = true; + }); + })); + + it('should call mutliple render hooks', async(() => { + const consoleSpy = spyOn(console, 'warn'); + renderModule(MultiRenderHookModule, {document: doc}).then(output => { + // title should be added by the render hook. + expect(output).toBe( + 'RenderHook' + + 'Works!'); + expect(consoleSpy).toHaveBeenCalled(); + called = true; + }); + })); }); describe('http', () => { diff --git a/tools/public_api_guard/platform-server/index.d.ts b/tools/public_api_guard/platform-server/index.d.ts index 4a0495b6da..242d062904 100644 --- a/tools/public_api_guard/platform-server/index.d.ts +++ b/tools/public_api_guard/platform-server/index.d.ts @@ -1,3 +1,6 @@ +/** @experimental */ +export declare const BEFORE_APP_SERIALIZED: InjectionToken<(() => void)[]>; + /** @experimental */ export declare const INITIAL_CONFIG: InjectionToken;