Permalink
Browse files

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
  • Loading branch information...
vikerman authored and mhevery committed Sep 4, 2017
1 parent 15945c8 commit 8dfc3c386af17977547500b9fb3b7974991a770a
@@ -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';
@@ -24,3 +24,12 @@ export interface PlatformConfig {
* @experimental
*/
export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('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<Array<() => void>>('Server.RENDER_MODULE_HOOK');
@@ -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;
});
@@ -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(
'<html><head><title>RenderHook</title></head><body>' +
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
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(
'<html><head><title>RenderHook</title><meta name="description"></head>' +
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
expect(consoleSpy).toHaveBeenCalled();
called = true;
});
}));
});
describe('http', () => {
@@ -1,3 +1,6 @@
/** @experimental */
export declare const BEFORE_APP_SERIALIZED: InjectionToken<(() => void)[]>;
/** @experimental */
export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>;

0 comments on commit 8dfc3c3

Please sign in to comment.