fix(platform-server): avoid clash between server and client style encapsulation attributes (#24158)

Previously the style encapsulation attributes(_nghost-* and _ngcontent-*) created on the server could overlap with the attributes and styles created by the client side app when it botstraps. In case the client is bootstrapping a lazy route, the client side styles are added before the server-side styles are removed. If the components on the client are bootstrapped in a different order than on the server, the styles generated by the client will cause the elements on the server to have the wrong styles.

The fix puts the styles and attributes generated on the server in a completely differemt space so that they are not affected by the client generated styles. The client generated styles will only affect elements bootstrapped on the client.

PR Close #24158
This commit is contained in:
Vikram Subramanian 2018-05-28 00:20:53 -07:00 committed by Victor Berchet
parent c917e5b5bb
commit b96a3c8def
4 changed files with 30 additions and 8 deletions

View File

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Inject, Injectable, NgZone} from '@angular/core'; import {isPlatformServer} from '@angular/common';
import {Inject, Injectable, NgZone, Optional, PLATFORM_ID} from '@angular/core';
// Import zero symbols from zone.js. This causes the zone ambient type to be // Import zero symbols from zone.js. This causes the zone ambient type to be
// added to the type-checker, without emitting any runtime module load statement // added to the type-checker, without emitting any runtime module load statement
import {} from 'zone.js'; import {} from 'zone.js';
@ -103,10 +106,14 @@ const globalListener = function(event: Event) {
@Injectable() @Injectable()
export class DomEventsPlugin extends EventManagerPlugin { export class DomEventsPlugin extends EventManagerPlugin {
constructor(@Inject(DOCUMENT) doc: any, private ngZone: NgZone) { constructor(
@Inject(DOCUMENT) doc: any, private ngZone: NgZone,
@Optional() @Inject(PLATFORM_ID) platformId: {}|null) {
super(doc); super(doc);
this.patchEvent(); if (!platformId || !isPlatformServer(platformId)) {
this.patchEvent();
}
} }
private patchEvent() { private patchEvent() {

View File

@ -24,7 +24,7 @@ import {el} from '../../../testing/src/browser_util';
beforeEach(() => { beforeEach(() => {
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
zone = new NgZone({}); zone = new NgZone({});
domEventPlugin = new DomEventsPlugin(doc, zone); domEventPlugin = new DomEventsPlugin(doc, zone, null);
}); });
it('should delegate event bindings to plugins that are passed in from the most generic one to the most specific one', it('should delegate event bindings to plugins that are passed in from the most generic one to the most specific one',

View File

@ -206,11 +206,13 @@ class EmulatedEncapsulationServerRenderer2 extends DefaultServerRenderer2 {
eventManager: EventManager, document: any, ngZone: NgZone, sharedStylesHost: SharedStylesHost, eventManager: EventManager, document: any, ngZone: NgZone, sharedStylesHost: SharedStylesHost,
schema: DomElementSchemaRegistry, private component: RendererType2) { schema: DomElementSchemaRegistry, private component: RendererType2) {
super(eventManager, document, ngZone, schema); super(eventManager, document, ngZone, schema);
const styles = flattenStyles(component.id, component.styles, []); // Add a 's' prefix to style attributes to indicate server.
const componentId = 's' + component.id;
const styles = flattenStyles(componentId, component.styles, []);
sharedStylesHost.addStyles(styles); sharedStylesHost.addStyles(styles);
this.contentAttr = shimContentAttribute(component.id); this.contentAttr = shimContentAttribute(componentId);
this.hostAttr = shimHostAttribute(component.id); this.hostAttr = shimHostAttribute(componentId);
} }
applyToHost(element: any) { super.setAttribute(element, this.hostAttr, ''); } applyToHost(element: any) { super.setAttribute(element, this.hostAttr, ''); }

View File

@ -154,7 +154,11 @@ class MyAnimationApp {
class AnimationServerModule { class AnimationServerModule {
} }
@Component({selector: 'app', template: `Works!`, styles: [':host { color: red; }']}) @Component({
selector: 'app',
template: `<div>Works!</div>`,
styles: ['div {color: blue; } :host { color: red; }']
})
class MyStylesApp { class MyStylesApp {
} }
@ -548,6 +552,15 @@ class EscapedTransferStoreModule {
}); });
})); }));
it('sets a prefix for the _nghost and _ngcontent attributes', async(() => {
renderModule(ExampleStylesModule, {document: doc}).then(output => {
expect(output).toMatch(
/<html><head><style ng-transition="example-styles">div\[_ngcontent-sc\d+\] {color: blue; } \[_nghost-sc\d+\] { color: red; }<\/style><\/head><body><app _nghost-sc\d+="" ng-version="0.0.0-PLACEHOLDER"><div _ngcontent-sc\d+="">Works!<\/div><\/app><\/body><\/html>/);
called = true;
});
}));
it('should handle false values on attributes', async(() => { it('should handle false values on attributes', async(() => {
renderModule(FalseAttributesModule, {document: doc}).then(output => { renderModule(FalseAttributesModule, {document: doc}).then(output => {
expect(output).toBe( expect(output).toBe(