Skip to content

Commit

Permalink
feat(platform-server): support document reference in render functions (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
AndrewKushnir authored and dylhunn committed Aug 4, 2022
1 parent de1e280 commit 2b4d7f6
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 11 deletions.
4 changes: 2 additions & 2 deletions goldens/public-api/platform-server/index.md
Expand Up @@ -51,15 +51,15 @@ export class PlatformState {
// @public
export function renderApplication<T>(rootComponent: Type<T>, options: {
appId: string;
document?: string;
document?: string | Document;
url?: string;
providers?: Array<Provider | ImportedNgModuleProviders>;
platformProviders?: Provider[];
}): Promise<string>;

// @public
export function renderModule<T>(module: Type<T>, options: {
document?: string;
document?: string | Document;
url?: string;
extraProviders?: StaticProvider[];
}): Promise<string>;
Expand Down
11 changes: 8 additions & 3 deletions packages/platform-server/src/server.ts
Expand Up @@ -81,9 +81,14 @@ export class ServerModule {
}

function _document(injector: Injector) {
let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
const document = config && config.document ? parseDocument(config.document, config.url) :
getDOM().createHtmlDocument();
const config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
let document: Document;
if (config && config.document) {
document = typeof config.document === 'string' ? parseDocument(config.document, config.url) :
config.document;
} else {
document = getDOM().createHtmlDocument();
}
// Tell ivy about the global document
ɵsetDocument(document);
return document;
Expand Down
13 changes: 8 additions & 5 deletions packages/platform-server/src/utils.ts
Expand Up @@ -16,7 +16,7 @@ import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
import {TRANSFER_STATE_SERIALIZATION_PROVIDERS} from './transfer_state';

interface PlatformOptions {
document?: string;
document?: string|Document;
url?: string;
platformProviders?: Provider[];
}
Expand Down Expand Up @@ -95,14 +95,16 @@ the server-rendered app can be properly bootstrapped into a client app.`);
/**
* Renders a Module to string.
*
* `document` is the full document HTML of the page to render, as a string.
* `document` is the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* `url` is the URL for the current render request.
* `extraProviders` are the platform level providers for the current render request.
*
* @publicApi
*/
export function renderModule<T>(
module: Type<T>, options: {document?: string, url?: string, extraProviders?: StaticProvider[]}):
module: Type<T>,
options: {document?: string|Document, url?: string, extraProviders?: StaticProvider[]}):
Promise<string> {
const {document, url, extraProviders: platformProviders} = options;
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
Expand Down Expand Up @@ -130,7 +132,8 @@ export function renderModule<T>(
* - `appId` - a string identifier of this application. The appId is used to prefix all
* server-generated stylings and state keys of the application in TransferState
* use-cases.
* - `document` - the full document HTML of the page to render, as a string.
* - `document` - the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* - `url` - the URL for the current render request.
* - `providers` - set of application level providers for the current render request.
* - `platformProviders` - the platform level providers for the current render request.
Expand All @@ -141,7 +144,7 @@ export function renderModule<T>(
*/
export function renderApplication<T>(rootComponent: Type<T>, options: {
appId: string,
document?: string,
document?: string|Document,
url?: string,
providers?: Array<Provider|ImportedNgModuleProviders>,
platformProviders?: Provider[],
Expand Down
34 changes: 33 additions & 1 deletion packages/platform-server/test/integration_spec.ts
Expand Up @@ -11,7 +11,7 @@ import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostBinding, HostListener, importProvidersFrom, Inject, Injectable, Input, NgModule, NgZone, OnInit, PLATFORM_ID, PlatformRef, Type, ViewEncapsulation} from '@angular/core';
import {inject, waitForAsync} from '@angular/core/testing';
import {inject, TestBed, waitForAsync} from '@angular/core/testing';
import {BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule, ServerTransferStateModule} from '@angular/platform-server';
import {Observable} from 'rxjs';
Expand Down Expand Up @@ -750,6 +750,38 @@ describe('platform-server integration', () => {
});
}));

it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} ` +
`should allow passing a document reference`,
waitForAsync(() => {
const document = TestBed.inject(DOCUMENT);

// Append root element based on the app selector.
const rootEl = document.createElement('app');
document.body.appendChild(rootEl);

// Append a special marker to verify that we use a correct instance
// of the document for rendering.
const markerEl = document.createComment('test marker');
document.body.appendChild(markerEl);

const options = {document};
const bootstrap = isStandalone ?
renderApplication(MyAsyncServerAppStandalone, {document, appId: 'simple-cmp'}) :
renderModule(AsyncServerModule, options);
bootstrap
.then(output => {
expect(output).toBe(
'<html><head><title>fakeTitle</title></head>' +
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 textcontent="fine">fine</h1></app>' +
'<!--test marker--></body></html>');
called = true;
})
.finally(() => {
rootEl.remove();
markerEl.remove();
});
}));

it('works with SVG elements', waitForAsync(() => {
const options = {document: doc};
const bootstrap = isStandalone ?
Expand Down

0 comments on commit 2b4d7f6

Please sign in to comment.