Skip to content

Commit 8dfc3c3

Browse files
vikermanmhevery
authored andcommitted
feat(platform-server): provide a way to hook into renderModule* (angular#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 angular#19023
1 parent 15945c8 commit 8dfc3c3

5 files changed

Lines changed: 97 additions & 4 deletions

File tree

packages/platform-server/src/platform-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
export {PlatformState} from './platform_state';
1010
export {ServerModule, platformDynamicServer, platformServer} from './server';
11-
export {INITIAL_CONFIG, PlatformConfig} from './tokens';
11+
export {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens';
1212
export {renderModule, renderModuleFactory} from './utils';
1313

1414
export * from './private_export';

packages/platform-server/src/tokens.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ export interface PlatformConfig {
2424
* @experimental
2525
*/
2626
export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('Server.INITIAL_CONFIG');
27+
28+
/**
29+
* A function that will be executed when calling `renderModuleFactory` or `renderModule` just
30+
* before current platform state is rendered to string.
31+
*
32+
* @experimental
33+
*/
34+
export const BEFORE_APP_SERIALIZED =
35+
new InjectionToken<Array<() => void>>('Server.RENDER_MODULE_HOOK');

packages/platform-server/src/utils.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {toPromise} from 'rxjs/operator/toPromise';
1414

1515
import {PlatformState} from './platform_state';
1616
import {platformDynamicServer, platformServer} from './server';
17-
import {INITIAL_CONFIG} from './tokens';
17+
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
1818

1919
interface PlatformOptions {
2020
document?: string;
@@ -45,7 +45,22 @@ the server-rendered app can be properly bootstrapped into a client app.`);
4545
return toPromise
4646
.call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable)))
4747
.then(() => {
48-
const output = platform.injector.get(PlatformState).renderToString();
48+
const platformState = platform.injector.get(PlatformState);
49+
50+
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
51+
const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
52+
if (callbacks) {
53+
for (const callback of callbacks) {
54+
try {
55+
callback();
56+
} catch (e) {
57+
// Ignore exceptions.
58+
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
59+
}
60+
}
61+
}
62+
63+
const output = platformState.renderToString();
4964
platform.destroy();
5065
return output;
5166
});

packages/platform-server/test/integration_spec.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {Http, HttpModule, Response, ResponseOptions, XHRBackend} from '@angular/
1616
import {MockBackend, MockConnection} from '@angular/http/testing';
1717
import {BrowserModule, DOCUMENT, Title} from '@angular/platform-browser';
1818
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
19-
import {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server';
19+
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server';
2020
import {Subscription} from 'rxjs/Subscription';
2121
import {filter} from 'rxjs/operator/filter';
2222
import {first} from 'rxjs/operator/first';
@@ -38,6 +38,50 @@ class MyServerApp {
3838
class ExampleModule {
3939
}
4040

41+
function getTitleRenderHook(doc: any) {
42+
return () => {
43+
// Set the title as part of the render hook.
44+
doc.title = 'RenderHook';
45+
};
46+
}
47+
48+
function exceptionRenderHook() {
49+
throw new Error('error');
50+
}
51+
52+
function getMetaRenderHook(doc: any) {
53+
return () => {
54+
// Add a meta tag before rendering the document.
55+
const metaElement = doc.createElement('meta');
56+
metaElement.setAttribute('name', 'description');
57+
doc.head.appendChild(metaElement);
58+
};
59+
}
60+
61+
@NgModule({
62+
bootstrap: [MyServerApp],
63+
declarations: [MyServerApp],
64+
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
65+
providers: [
66+
{provide: BEFORE_APP_SERIALIZED, useFactory: getTitleRenderHook, multi: true, deps: [DOCUMENT]},
67+
]
68+
})
69+
class RenderHookModule {
70+
}
71+
72+
@NgModule({
73+
bootstrap: [MyServerApp],
74+
declarations: [MyServerApp],
75+
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
76+
providers: [
77+
{provide: BEFORE_APP_SERIALIZED, useFactory: getTitleRenderHook, multi: true, deps: [DOCUMENT]},
78+
{provide: BEFORE_APP_SERIALIZED, useValue: exceptionRenderHook, multi: true},
79+
{provide: BEFORE_APP_SERIALIZED, useFactory: getMetaRenderHook, multi: true, deps: [DOCUMENT]},
80+
]
81+
})
82+
class MultiRenderHookModule {
83+
}
84+
4185
@Component({selector: 'app', template: `Works too!`})
4286
class MyServerApp2 {
4387
}
@@ -469,6 +513,28 @@ export function main() {
469513
called = true;
470514
});
471515
}));
516+
517+
it('should call render hook', async(() => {
518+
renderModule(RenderHookModule, {document: doc}).then(output => {
519+
// title should be added by the render hook.
520+
expect(output).toBe(
521+
'<html><head><title>RenderHook</title></head><body>' +
522+
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
523+
called = true;
524+
});
525+
}));
526+
527+
it('should call mutliple render hooks', async(() => {
528+
const consoleSpy = spyOn(console, 'warn');
529+
renderModule(MultiRenderHookModule, {document: doc}).then(output => {
530+
// title should be added by the render hook.
531+
expect(output).toBe(
532+
'<html><head><title>RenderHook</title><meta name="description"></head>' +
533+
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
534+
expect(consoleSpy).toHaveBeenCalled();
535+
called = true;
536+
});
537+
}));
472538
});
473539

474540
describe('http', () => {

tools/public_api_guard/platform-server/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/** @experimental */
2+
export declare const BEFORE_APP_SERIALIZED: InjectionToken<(() => void)[]>;
3+
14
/** @experimental */
25
export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>;
36

0 commit comments

Comments
 (0)