fix(platform-server): render styles in app component instead of <head>

This ensures when the tree is serialized to the client and the app is later bootstrapped,
the <style> tags created during server-side rendering are destroyed.
This commit is contained in:
Alex Rickabaugh 2017-02-14 11:34:05 -08:00 committed by Igor Minar
parent 17486fd696
commit 30380d010b
4 changed files with 84 additions and 5 deletions

View File

@ -8,15 +8,15 @@
import {PlatformLocation} from '@angular/common'; import {PlatformLocation} from '@angular/common';
import {platformCoreDynamic} from '@angular/compiler'; import {platformCoreDynamic} from '@angular/compiler';
import {InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RENDERER_V2_DIRECT, RendererV2, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; import {APP_BOOTSTRAP_LISTENER, InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RENDERER_V2_DIRECT, RendererV2, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
import {BrowserModule, DOCUMENT} from '@angular/platform-browser'; import {BrowserModule, DOCUMENT} from '@angular/platform-browser';
import {ServerPlatformLocation} from './location'; import {ServerPlatformLocation} from './location';
import {Parse5DomAdapter, parseDocument} from './parse5_adapter'; import {Parse5DomAdapter, parseDocument} from './parse5_adapter';
import {PlatformState} from './platform_state'; import {PlatformState} from './platform_state';
import {ALLOW_MULTIPLE_PLATFORMS, DebugDomRendererV2, DebugDomRootRenderer} from './private_import_core'; import {ALLOW_MULTIPLE_PLATFORMS, DebugDomRendererV2, DebugDomRootRenderer} from './private_import_core';
import {SharedStylesHost, getDOM} from './private_import_platform-browser'; import {SharedStylesHost, getDOM} from './private_import_platform-browser';
import {ServerRendererV2, ServerRootRenderer} from './server_renderer'; import {ServerRendererV2, ServerRootRenderer} from './server_renderer';
import {ServerStylesHost} from './styles_host';
function notSupported(feature: string): Error { function notSupported(feature: string): Error {
throw new Error(`platform-server does not support '${feature}'.`); throw new Error(`platform-server does not support '${feature}'.`);
@ -42,12 +42,24 @@ export function _createDebugRendererV2(renderer: RendererV2): RendererV2 {
return isDevMode() ? new DebugDomRendererV2(renderer) : renderer; return isDevMode() ? new DebugDomRendererV2(renderer) : renderer;
} }
export function _addStylesToRootComponentFactory(stylesHost: ServerStylesHost) {
const initializer = () => stylesHost.rootComponentIsReady();
return initializer;
}
export const SERVER_RENDER_PROVIDERS: Provider[] = [ export const SERVER_RENDER_PROVIDERS: Provider[] = [
ServerRootRenderer, {provide: RENDERER_V2_DIRECT, useClass: ServerRendererV2}, ServerRootRenderer,
{provide: RENDERER_V2_DIRECT, useClass: ServerRendererV2},
{provide: RendererV2, useFactory: _createDebugRendererV2, deps: [RENDERER_V2_DIRECT]}, {provide: RendererV2, useFactory: _createDebugRendererV2, deps: [RENDERER_V2_DIRECT]},
{provide: RootRenderer, useFactory: _createConditionalRootRenderer, deps: [ServerRootRenderer]}, {provide: RootRenderer, useFactory: _createConditionalRootRenderer, deps: [ServerRootRenderer]},
// use plain SharedStylesHost, not the DomSharedStylesHost ServerStylesHost,
SharedStylesHost {provide: SharedStylesHost, useExisting: ServerStylesHost},
{
provide: APP_BOOTSTRAP_LISTENER,
useFactory: _addStylesToRootComponentFactory,
deps: [ServerStylesHost],
multi: true
},
]; ];
/** /**

View File

@ -47,6 +47,7 @@ export class ServerRenderer implements Renderer {
if (componentProto.encapsulation === ViewEncapsulation.Native) { if (componentProto.encapsulation === ViewEncapsulation.Native) {
throw new Error('Native encapsulation is not supported on the server!'); throw new Error('Native encapsulation is not supported on the server!');
} }
this._rootRenderer.sharedStylesHost.addStyles(this._styles);
if (this.componentProto.encapsulation === ViewEncapsulation.Emulated) { if (this.componentProto.encapsulation === ViewEncapsulation.Emulated) {
this._contentAttr = shimContentAttribute(styleShimId); this._contentAttr = shimContentAttribute(styleShimId);
this._hostAttr = shimHostAttribute(styleShimId); this._hostAttr = shimHostAttribute(styleShimId);

View File

@ -0,0 +1,45 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef, Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {Parse5DomAdapter} from './parse5_adapter';
import {SharedStylesHost, getDOM} from './private_import_platform-browser';
@Injectable()
export class ServerStylesHost extends SharedStylesHost {
private root: any = null;
private buffer: string[] = [];
constructor(@Inject(DOCUMENT) private doc: any, private appRef: ApplicationRef) { super(); }
private _addStyle(style: string): void {
let adapter: Parse5DomAdapter = getDOM() as Parse5DomAdapter;
const el = adapter.createElement('style');
adapter.setText(el, style);
adapter.appendChild(this.root, el);
}
onStylesAdded(additions: Set<string>) {
if (!this.root) {
additions.forEach(style => this.buffer.push(style));
} else {
additions.forEach(style => this._addStyle(style));
}
}
rootComponentIsReady(): void {
if (!!this.root) {
return;
}
this.root = this.appRef.components[0].location.nativeElement;
this.buffer.forEach(style => this._addStyle(style));
this.buffer = null;
}
}

View File

@ -47,6 +47,14 @@ class MyAsyncServerApp {
class AsyncServerModule { class AsyncServerModule {
} }
@Component({selector: 'app', template: `Works!`, styles: [':host { color: red; }']})
class MyStylesApp {
}
@NgModule({declarations: [MyStylesApp], imports: [ServerModule], bootstrap: [MyStylesApp]})
class ExampleStylesModule {
}
export function main() { export function main() {
if (getDOM().supportsDOMEvents()) return; // NODE only if (getDOM().supportsDOMEvents()) return; // NODE only
@ -87,6 +95,19 @@ export function main() {
}); });
})); }));
it('adds styles to the root component', async(() => {
const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
platform.bootstrapModule(ExampleStylesModule).then(ref => {
const appRef: ApplicationRef = ref.injector.get(ApplicationRef);
const app = appRef.components[0].location.nativeElement;
expect(app.children.length).toBe(2);
const style = app.children[1];
expect(style.type).toBe('style');
expect(style.children[0].data).toContain('color: red');
});
}));
describe('PlatformLocation', () => { describe('PlatformLocation', () => {
it('is injectable', async(() => { it('is injectable', async(() => {
const platform = platformDynamicServer( const platform = platformDynamicServer(