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.