feat(compiler): implement style encapsulation for new view engine (#14518)

Included refactoring:
- splits the `RendererV2` into a `RendererFactoryV2` and a `RendererV2`
- makes the `DebugRendererV2` a private class in `@angular/core`
- remove `setBindingDebugInfo` from `RendererV2`, but rename `RendererV2.setText` to 
  `RendererV2.setValue` and allow it on comments and text nodes.

Part of #14013
This commit is contained in:
Tobias Bosch
2017-02-16 13:55:55 -08:00
committed by Igor Minar
parent ba17dcbf2b
commit 0fa3895d5b
38 changed files with 828 additions and 595 deletions

View File

@ -7,7 +7,7 @@
*/
import {CommonModule, PlatformLocation} from '@angular/common';
import {ApplicationModule, ErrorHandler, NgModule, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, RENDERER_V2_DIRECT, RendererV2, RootRenderer, Sanitizer, SkipSelf, Testability, createPlatformFactory, platformCore} from '@angular/core';
import {ApplicationModule, ErrorHandler, NgModule, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactoryV2, RootRenderer, Sanitizer, SkipSelf, Testability, createPlatformFactory, platformCore} from '@angular/core';
import {AnimationDriver} from '../src/dom/animation_driver';
import {WebAnimationsDriver} from '../src/dom/web_animations_driver';
@ -19,7 +19,7 @@ import {BrowserGetTestability} from './browser/testability';
import {Title} from './browser/title';
import {ELEMENT_PROBE_PROVIDERS} from './dom/debug/ng_probe';
import {getDOM} from './dom/dom_adapter';
import {DomRendererV2, DomRootRenderer, DomRootRenderer_} from './dom/dom_renderer';
import {DomRendererFactoryV2, DomRootRenderer, DomRootRenderer_} from './dom/dom_renderer';
import {DOCUMENT} from './dom/dom_tokens';
import {DomEventsPlugin} from './dom/events/dom_events';
import {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
@ -86,8 +86,8 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
{provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig},
{provide: DomRootRenderer, useClass: DomRootRenderer_},
{provide: RootRenderer, useExisting: DomRootRenderer},
{provide: RENDERER_V2_DIRECT, useClass: DomRendererV2},
{provide: RendererV2, useExisting: RENDERER_V2_DIRECT},
DomRendererFactoryV2,
{provide: RendererFactoryV2, useExisting: DomRendererFactoryV2},
{provide: SharedStylesHost, useExisting: DomSharedStylesHost},
{provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver},
DomSharedStylesHost,

View File

@ -9,9 +9,9 @@
import * as core from '@angular/core';
import {StringMapWrapper} from '../../facade/collection';
import {DebugDomRendererV2, DebugDomRootRenderer} from '../../private_import_core';
import {DebugDomRootRenderer} from '../../private_import_core';
import {getDOM} from '../dom_adapter';
import {DomRootRenderer} from '../dom_renderer';
import {DomRendererFactoryV2, DomRootRenderer} from '../dom_renderer';
const CORE_TOKENS = {
'ApplicationRef': core.ApplicationRef,
@ -58,10 +58,6 @@ function _ngProbeTokensToMap(tokens: NgProbeToken[]): {[name: string]: any} {
return tokens.reduce((prev: any, t: any) => (prev[t.name] = t.token, prev), {});
}
export function _createDebugRendererV2(renderer: core.RendererV2): core.RendererV2 {
return core.isDevMode() ? new DebugDomRendererV2(renderer) : renderer;
}
/**
* Providers which support debugging Angular applications (e.g. via `ng.probe`).
*/
@ -75,9 +71,4 @@ export const ELEMENT_PROBE_PROVIDERS: core.Provider[] = [
[core.NgProbeToken, new core.Optional()],
],
},
{
provide: core.RendererV2,
useFactory: _createDebugRendererV2,
deps: [core.RENDERER_V2_DIRECT],
}
];

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {APP_ID, Inject, Injectable, RenderComponentType, Renderer, RendererV2, RootRenderer, ViewEncapsulation} from '@angular/core';
import {APP_ID, ComponentRenderTypeV2, Inject, Injectable, RenderComponentType, Renderer, RendererFactoryV2, RendererV2, RootRenderer, ViewEncapsulation} from '@angular/core';
import {isPresent, stringify} from '../facade/lang';
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, DirectRenderer, NoOpAnimationPlayer, RenderDebugInfo} from '../private_import_core';
@ -229,13 +229,8 @@ export class DomRenderer implements Renderer {
TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(parsedBindings, null, 2));
} else {
// Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers
if (propertyName[propertyName.length - 1] === '$') {
const attrNode: Attr = createAttributeNode(propertyName).cloneNode(true) as Attr;
attrNode.value = propertyValue;
renderElement.setAttributeNode(attrNode);
} else {
this.setElementAttribute(renderElement, propertyName, propertyValue);
}
propertyName = propertyName.replace(/\$/g, '_');
this.setElementAttribute(renderElement, propertyName, propertyValue);
}
}
@ -366,10 +361,51 @@ function createAttributeNode(name: string): Attr {
}
@Injectable()
export class DomRendererV2 implements RendererV2 {
constructor(private eventManager: EventManager){};
export class DomRendererFactoryV2 implements RendererFactoryV2 {
private rendererByCompId = new Map<string, RendererV2>();
private defaultRenderer: RendererV2;
createElement(name: string, namespace?: string, debugInfo?: any): any {
constructor(private eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost) {
this.defaultRenderer = new DefaultDomRendererV2(eventManager);
};
createRenderer(element: any, type: ComponentRenderTypeV2): RendererV2 {
if (!element || !type) {
return this.defaultRenderer;
}
switch (type.encapsulation) {
case ViewEncapsulation.Emulated: {
let renderer = this.rendererByCompId.get(type.id);
if (!renderer) {
renderer = new EmulatedEncapsulationDomRendererV2(
this.eventManager, this.sharedStylesHost, type);
this.rendererByCompId.set(type.id, renderer);
}
(<EmulatedEncapsulationDomRendererV2>renderer).applyToHost(element);
return renderer;
}
case ViewEncapsulation.Native:
return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type);
default: {
if (!this.rendererByCompId.has(type.id)) {
const styles = flattenStyles(type.id, type.styles, []);
this.sharedStylesHost.addStyles(styles);
this.rendererByCompId.set(type.id, this.defaultRenderer);
}
return this.defaultRenderer;
}
}
}
}
class DefaultDomRendererV2 implements RendererV2 {
constructor(private eventManager: EventManager) {}
destroy(): void {}
destroyNode: null;
createElement(name: string, namespace?: string): any {
if (namespace) {
return document.createElementNS(NAMESPACE_URIS[namespace], name);
}
@ -377,9 +413,9 @@ export class DomRendererV2 implements RendererV2 {
return document.createElement(name);
}
createComment(value: string, debugInfo?: any): any { return document.createComment(value); }
createComment(value: string): any { return document.createComment(value); }
createText(value: string, debugInfo?: any): any { return document.createTextNode(value); }
createText(value: string): any { return document.createTextNode(value); }
appendChild(parent: any, newChild: any): void { parent.appendChild(newChild); }
@ -395,7 +431,7 @@ export class DomRendererV2 implements RendererV2 {
}
}
selectRootElement(selectorOrNode: string|any, debugInfo?: any): any {
selectRootElement(selectorOrNode: string|any): any {
let el: any = typeof selectorOrNode === 'string' ? document.querySelector(selectorOrNode) :
selectorOrNode;
el.textContent = '';
@ -422,43 +458,6 @@ export class DomRendererV2 implements RendererV2 {
}
}
setBindingDebugInfo(el: any, propertyName: string, propertyValue: string): void {
if (el.nodeType === Node.COMMENT_NODE) {
const m = el.nodeValue.replace(/\n/g, '').match(TEMPLATE_BINDINGS_EXP);
const obj = m === null ? {} : JSON.parse(m[1]);
obj[propertyName] = propertyValue;
el.nodeValue = TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(obj, null, 2));
} else {
// Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers
if (propertyName[propertyName.length - 1] === '$') {
const attrNode: Attr = createAttributeNode(propertyName).cloneNode(true) as Attr;
attrNode.value = propertyValue;
el.setAttributeNode(attrNode);
} else {
this.setAttribute(el, propertyName, propertyValue);
}
}
}
removeBindingDebugInfo(el: any, propertyName: string): void {
if (el.nodeType === Node.COMMENT_NODE) {
const m = el.nodeValue.replace(/\n/g, '').match(TEMPLATE_BINDINGS_EXP);
const obj = m === null ? {} : JSON.parse(m[1]);
delete obj[propertyName];
el.nodeValue = TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(obj, null, 2));
} else {
// Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers
if (propertyName[propertyName.length - 1] === '$') {
const attrNode: Attr = createAttributeNode(propertyName).cloneNode(true) as Attr;
attrNode.value = '';
el.setAttributeNode(attrNode);
} else {
this.removeAttribute(el, propertyName);
}
}
}
addClass(el: any, name: string): void { el.classList.add(name); }
removeClass(el: any, name: string): void { el.classList.remove(name); }
@ -476,7 +475,7 @@ export class DomRendererV2 implements RendererV2 {
setProperty(el: any, name: string, value: any): void { el[name] = value; }
setText(node: any, value: string): void { node.nodeValue = value; }
setValue(node: any, value: string): void { node.nodeValue = value; }
listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
() => void {
@ -488,3 +487,62 @@ export class DomRendererV2 implements RendererV2 {
target, event, decoratePreventDefault(callback)) as() => void;
}
}
class EmulatedEncapsulationDomRendererV2 extends DefaultDomRendererV2 {
private contentAttr: string;
private hostAttr: string;
constructor(
eventManager: EventManager, sharedStylesHost: DomSharedStylesHost,
private component: ComponentRenderTypeV2) {
super(eventManager);
const styles = flattenStyles(component.id, component.styles, []);
sharedStylesHost.addStyles(styles);
this.contentAttr = shimContentAttribute(component.id);
this.hostAttr = shimHostAttribute(component.id);
}
applyToHost(element: any) { super.setAttribute(element, this.hostAttr, ''); }
createElement(parent: any, name: string): Element {
const el = super.createElement(parent, name);
super.setAttribute(el, this.contentAttr, '');
return el;
}
}
class ShadowDomRenderer extends DefaultDomRendererV2 {
private shadowRoot: any;
constructor(
eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost,
private hostEl: any, private component: ComponentRenderTypeV2) {
super(eventManager);
this.shadowRoot = (hostEl as any).createShadowRoot();
this.sharedStylesHost.addHost(this.shadowRoot);
const styles = flattenStyles(component.id, component.styles, []);
for (let i = 0; i < styles.length; i++) {
const styleEl = document.createElement('style');
styleEl.textContent = styles[i];
this.shadowRoot.appendChild(styleEl);
}
}
private nodeOrShadowRoot(node: any): any { return node === this.hostEl ? this.shadowRoot : node; }
destroy() { this.sharedStylesHost.removeHost(this.shadowRoot); }
appendChild(parent: any, newChild: any): void {
return super.appendChild(this.nodeOrShadowRoot(parent), newChild);
}
insertBefore(parent: any, newChild: any, refChild: any): void {
return super.insertBefore(this.nodeOrShadowRoot(parent), newChild, refChild);
}
removeChild(parent: any, oldChild: any): void {
return super.removeChild(this.nodeOrShadowRoot(parent), oldChild);
}
parentNode(node: any): any {
return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(node)));
}
}

View File

@ -16,8 +16,6 @@ export const ReflectionCapabilities: typeof r.ReflectionCapabilities = r.Reflect
export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer;
export const DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer;
export type DebugDomRendererV2 = typeof r._DebugDomRendererV2;
export const DebugDomRendererV2: typeof r.DebugDomRendererV2 = r.DebugDomRendererV2;
export const reflector: typeof r.reflector = r.reflector;