Skip to content
Permalink
Browse files

feat(platform-server): wait on returned BEFORE_APP_SERIALIZED promises (

#29120)

This update gives external tooling the ability for async providers to
finish resolving before the document is serialized. This is not a
breaking change since render already returns a promise. All returned
promises from `BEFORE_APP_SERIALIZED` providers will wait to be
resolved or rejected. Any rejected promises will only console.warn().

PR Close #29120
  • Loading branch information...
adamdbradley authored and AndrewKushnir committed Mar 5, 2019
1 parent 6b98b53 commit 7102ea80a91edeecc1645ebe108033c8cb63e4d3
Showing with 97 additions and 5 deletions.
  1. +24 −5 packages/platform-server/src/utils.ts
  2. +73 −0 packages/platform-server/test/integration_spec.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type} from '@angular/core';
import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type, ɵisPromise} from '@angular/core';
importTRANSITION_ID} from '@angular/platform-browser';
import {first} from 'rxjs/operators';

@@ -45,22 +45,41 @@ the server-rendered app can be properly bootstrapped into a client app.`);
.then(() => {
const platformState = platform.injector.get(PlatformState);

const asyncPromises: Promise<any>[] = [];

// 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();
const callbackResult = callback();
ifisPromise(callbackResult)) {
asyncPromises.push(callbackResult);
}

} catch (e) {
// Ignore exceptions.
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
}
}
}

const output = platformState.renderToString();
platform.destroy();
return output;
const complete = () => {
const output = platformState.renderToString();
platform.destroy();
return output;
};

if (asyncPromises.length === 0) {
return complete();
}

return Promise
.all(asyncPromises.map(asyncPromise => {
return asyncPromise.catch(
e => { console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e); });
}))
.then(complete);
});
});
}
@@ -57,6 +57,24 @@ function getMetaRenderHook(doc: any) {
};
}

function getAsyncTitleRenderHook(doc: any) {
return () => {
// Async set the title as part of the render hook.
return new Promise(resolve => {
setTimeout(() => {
doc.title = 'AsyncRenderHook';
resolve();
});
});
};
}

function asyncRejectRenderHook() {
return () => {
return new Promise((_resolve, reject) => { setTimeout(() => { reject('reject'); }); });
};
}

@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
@@ -81,6 +99,39 @@ class RenderHookModule {
class MultiRenderHookModule {
}

@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
providers: [
{
provide: BEFORE_APP_SERIALIZED,
useFactory: getAsyncTitleRenderHook,
multi: true,
deps: [DOCUMENT]
},
]
})
class AsyncRenderHookModule {
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
providers: [
{provide: BEFORE_APP_SERIALIZED, useFactory: getMetaRenderHook, multi: true, deps: [DOCUMENT]},
{
provide: BEFORE_APP_SERIALIZED,
useFactory: getAsyncTitleRenderHook,
multi: true,
deps: [DOCUMENT]
},
{provide: BEFORE_APP_SERIALIZED, useFactory: asyncRejectRenderHook, multi: true},
]
})
class AsyncMultiRenderHookModule {
}

@Component({selector: 'app', template: `Works too!`})
class MyServerApp2 {
}
@@ -699,6 +750,28 @@ class HiddenModule {
called = true;
});
}));

it('should call async render hooks', async(() => {
renderModule(AsyncRenderHookModule, {document: doc}).then(output => {
// title should be added by the render hook.
expect(output).toBe(
'<html><head><title>AsyncRenderHook</title></head><body>' +
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
called = true;
});
}));

it('should call multiple async and sync render hooks', async(() => {
const consoleSpy = spyOn(console, 'warn');
renderModule(AsyncMultiRenderHookModule, {document: doc}).then(output => {
// title should be added by the render hook.
expect(output).toBe(
'<html><head><meta name="description"><title>AsyncRenderHook</title></head>' +
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
expect(consoleSpy).toHaveBeenCalled();
called = true;
});
}));
});

describe('http', () => {

0 comments on commit 7102ea8

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.