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;