Skip to content

Commit 2b4d7f6

Browse files
AndrewKushnirdylhunn
authored andcommitted
feat(platform-server): support document reference in render functions (#47032)
This commit updates the `renderModule` and `renderApplication` functions to also accept a document reference (in addition to the serialized document contents as a string). This should provide the necessary flexibility to NgUniversal (and other API consumers) to structure the logic to avoid serializing and parsing document multiple times during the SSR request. PR Close #47032
1 parent de1e280 commit 2b4d7f6

File tree

4 files changed

+51
-11
lines changed

4 files changed

+51
-11
lines changed

goldens/public-api/platform-server/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ export class PlatformState {
5151
// @public
5252
export function renderApplication<T>(rootComponent: Type<T>, options: {
5353
appId: string;
54-
document?: string;
54+
document?: string | Document;
5555
url?: string;
5656
providers?: Array<Provider | ImportedNgModuleProviders>;
5757
platformProviders?: Provider[];
5858
}): Promise<string>;
5959

6060
// @public
6161
export function renderModule<T>(module: Type<T>, options: {
62-
document?: string;
62+
document?: string | Document;
6363
url?: string;
6464
extraProviders?: StaticProvider[];
6565
}): Promise<string>;

packages/platform-server/src/server.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,14 @@ export class ServerModule {
8181
}
8282

8383
function _document(injector: Injector) {
84-
let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
85-
const document = config && config.document ? parseDocument(config.document, config.url) :
86-
getDOM().createHtmlDocument();
84+
const config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
85+
let document: Document;
86+
if (config && config.document) {
87+
document = typeof config.document === 'string' ? parseDocument(config.document, config.url) :
88+
config.document;
89+
} else {
90+
document = getDOM().createHtmlDocument();
91+
}
8792
// Tell ivy about the global document
8893
ɵsetDocument(document);
8994
return document;

packages/platform-server/src/utils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
1616
import {TRANSFER_STATE_SERIALIZATION_PROVIDERS} from './transfer_state';
1717

1818
interface PlatformOptions {
19-
document?: string;
19+
document?: string|Document;
2020
url?: string;
2121
platformProviders?: Provider[];
2222
}
@@ -95,14 +95,16 @@ the server-rendered app can be properly bootstrapped into a client app.`);
9595
/**
9696
* Renders a Module to string.
9797
*
98-
* `document` is the full document HTML of the page to render, as a string.
98+
* `document` is the document of the page to render, either as an HTML string or
99+
* as a reference to the `document` instance.
99100
* `url` is the URL for the current render request.
100101
* `extraProviders` are the platform level providers for the current render request.
101102
*
102103
* @publicApi
103104
*/
104105
export function renderModule<T>(
105-
module: Type<T>, options: {document?: string, url?: string, extraProviders?: StaticProvider[]}):
106+
module: Type<T>,
107+
options: {document?: string|Document, url?: string, extraProviders?: StaticProvider[]}):
106108
Promise<string> {
107109
const {document, url, extraProviders: platformProviders} = options;
108110
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
@@ -130,7 +132,8 @@ export function renderModule<T>(
130132
* - `appId` - a string identifier of this application. The appId is used to prefix all
131133
* server-generated stylings and state keys of the application in TransferState
132134
* use-cases.
133-
* - `document` - the full document HTML of the page to render, as a string.
135+
* - `document` - the document of the page to render, either as an HTML string or
136+
* as a reference to the `document` instance.
134137
* - `url` - the URL for the current render request.
135138
* - `providers` - set of application level providers for the current render request.
136139
* - `platformProviders` - the platform level providers for the current render request.
@@ -141,7 +144,7 @@ export function renderModule<T>(
141144
*/
142145
export function renderApplication<T>(rootComponent: Type<T>, options: {
143146
appId: string,
144-
document?: string,
147+
document?: string|Document,
145148
url?: string,
146149
providers?: Array<Provider|ImportedNgModuleProviders>,
147150
platformProviders?: Provider[],

packages/platform-server/test/integration_spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '
1111
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
1212
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
1313
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostBinding, HostListener, importProvidersFrom, Inject, Injectable, Input, NgModule, NgZone, OnInit, PLATFORM_ID, PlatformRef, Type, ViewEncapsulation} from '@angular/core';
14-
import {inject, waitForAsync} from '@angular/core/testing';
14+
import {inject, TestBed, waitForAsync} from '@angular/core/testing';
1515
import {BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
1616
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule, ServerTransferStateModule} from '@angular/platform-server';
1717
import {Observable} from 'rxjs';
@@ -750,6 +750,38 @@ describe('platform-server integration', () => {
750750
});
751751
}));
752752

753+
it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} ` +
754+
`should allow passing a document reference`,
755+
waitForAsync(() => {
756+
const document = TestBed.inject(DOCUMENT);
757+
758+
// Append root element based on the app selector.
759+
const rootEl = document.createElement('app');
760+
document.body.appendChild(rootEl);
761+
762+
// Append a special marker to verify that we use a correct instance
763+
// of the document for rendering.
764+
const markerEl = document.createComment('test marker');
765+
document.body.appendChild(markerEl);
766+
767+
const options = {document};
768+
const bootstrap = isStandalone ?
769+
renderApplication(MyAsyncServerAppStandalone, {document, appId: 'simple-cmp'}) :
770+
renderModule(AsyncServerModule, options);
771+
bootstrap
772+
.then(output => {
773+
expect(output).toBe(
774+
'<html><head><title>fakeTitle</title></head>' +
775+
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 textcontent="fine">fine</h1></app>' +
776+
'<!--test marker--></body></html>');
777+
called = true;
778+
})
779+
.finally(() => {
780+
rootEl.remove();
781+
markerEl.remove();
782+
});
783+
}));
784+
753785
it('works with SVG elements', waitForAsync(() => {
754786
const options = {document: doc};
755787
const bootstrap = isStandalone ?

0 commit comments

Comments
 (0)