diff --git a/modules/angular2/src/core/compiler/pipeline/default_steps.js b/modules/angular2/src/core/compiler/pipeline/default_steps.js index a4c41390af..11ee7cc176 100644 --- a/modules/angular2/src/core/compiler/pipeline/default_steps.js +++ b/modules/angular2/src/core/compiler/pipeline/default_steps.js @@ -12,7 +12,7 @@ import {ElementBinderBuilder} from './element_binder_builder'; import {ResolveCss} from './resolve_css'; import {ShimShadowDom} from './shim_shadow_dom'; import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; -import {ShadowDomStrategy, EmulatedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {ShadowDomStrategy, EmulatedScopedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; /** * Default steps used for compiling a template. @@ -39,7 +39,7 @@ export function createDefaultSteps( new ElementBinderBuilder(parser), ]; - if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) { + if (shadowDomStrategy instanceof EmulatedScopedShadowDomStrategy) { var step = new ShimShadowDom(compiledComponent, shadowDomStrategy); ListWrapper.push(steps, step); } diff --git a/modules/angular2/src/core/compiler/shadow_dom_strategy.js b/modules/angular2/src/core/compiler/shadow_dom_strategy.js index 77fd325395..925e82b219 100644 --- a/modules/angular2/src/core/compiler/shadow_dom_strategy.js +++ b/modules/angular2/src/core/compiler/shadow_dom_strategy.js @@ -23,15 +23,22 @@ export class ShadowDomStrategy { shimHostElement(component: Type, element: Element) {} } -export class EmulatedShadowDomStrategy extends ShadowDomStrategy { - _styleInliner: StyleInliner; +/** + * This strategy emulates the Shadow DOM for the templates, styles **excluded**: + * - components templates are added as children of their component element, + * - styles are moved from the templates to the styleHost (i.e. the document head). + * + * Notes: + * - styles are **not** scoped to their component and will apply to the whole document, + * - you can **not** use shadow DOM specific selectors in the styles + */ +export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy { _styleUrlResolver: StyleUrlResolver; - _styleHost: Element; _lastInsertedStyle: StyleElement; + _styleHost: Element; - constructor(styleInliner: StyleInliner, styleUrlResolver: StyleUrlResolver, styleHost: Element) { + constructor(styleUrlResolver: StyleUrlResolver, styleHost: Element) { super(); - this._styleInliner = styleInliner; this._styleUrlResolver = styleUrlResolver; this._styleHost = styleHost; } @@ -49,6 +56,58 @@ export class EmulatedShadowDomStrategy extends ShadowDomStrategy { return [Content]; } + transformStyleText(cssText: string, baseUrl: string, component: Type) { + return this._styleUrlResolver.resolveUrls(cssText, baseUrl); + } + + handleStyleElement(styleEl: StyleElement) { + DOM.remove(styleEl); + + var cssText = DOM.getText(styleEl); + + if (!MapWrapper.contains(_sharedStyleTexts, cssText)) { + // Styles are unscoped and shared across components, only append them to the head + // when there are not present yet + MapWrapper.set(_sharedStyleTexts, cssText, true); + this._insertStyleElement(this._styleHost, styleEl); + } + }; + + _insertStyleElement(host: Element, style: StyleElement) { + if (isBlank(this._lastInsertedStyle)) { + var firstChild = DOM.firstChild(host); + if (isPresent(firstChild)) { + DOM.insertBefore(firstChild, style); + } else { + DOM.appendChild(host, style); + } + } else { + DOM.insertAfter(this._lastInsertedStyle, style); + } + this._lastInsertedStyle = style; + } +} + +/** + * This strategy emulates the Shadow DOM for the templates, styles **included**: + * - components templates are added as children of their component element, + * - both the template and the styles are modified so that styles are scoped to the component + * they belong to, + * - styles are moved from the templates to the styleHost (i.e. the document head). + * + * Notes: + * - styles are scoped to their component and will apply only to it, + * - a common subset of shadow DOM selectors are supported, + * - see `ShadowCss` for more information and limitations. + */ +export class EmulatedScopedShadowDomStrategy extends EmulatedUnscopedShadowDomStrategy { + _styleInliner: StyleInliner; + + constructor(styleInliner: StyleInliner, styleUrlResolver: StyleUrlResolver, styleHost: Element) { + super(styleUrlResolver, styleHost); + this._styleInliner = styleInliner; + } + transformStyleText(cssText: string, baseUrl: string, component: Type) { cssText = this._styleUrlResolver.resolveUrls(cssText, baseUrl); var css = this._styleInliner.inlineImports(cssText, baseUrl); @@ -75,22 +134,14 @@ export class EmulatedShadowDomStrategy extends ShadowDomStrategy { var attrName = _getHostAttribute(id); DOM.setAttribute(element, attrName, ''); } - - _insertStyleElement(host: Element, style: StyleElement) { - if (isBlank(this._lastInsertedStyle)) { - var firstChild = DOM.firstChild(host); - if (isPresent(firstChild)) { - DOM.insertBefore(firstChild, style); - } else { - DOM.appendChild(host, style); - } - } else { - DOM.insertAfter(this._lastInsertedStyle, style); - } - this._lastInsertedStyle = style; - } } +/** + * This strategies uses the native Shadow DOM support. + * + * The templates for the component are inserted in a Shadow Root created on the component element. + * Hence they are strictly isolated. + */ export class NativeShadowDomStrategy extends ShadowDomStrategy { _styleUrlResolver: StyleUrlResolver; @@ -124,6 +175,7 @@ function _moveViewNodesIntoParent(parent, view) { var _componentUIDs: Map = MapWrapper.create(); var _nextComponentUID: int = 0; +var _sharedStyleTexts: Map = MapWrapper.create(); function _getComponentId(component: Type) { var id = MapWrapper.get(_componentUIDs, component); @@ -150,8 +202,9 @@ function _shimCssForComponent(cssText: string, component: Type): string { return shadowCss.shimCssText(cssText, _getContentAttribute(id), _getHostAttribute(id)); } -// Reset the component cache - used for tests only +// Reset the caches - used for tests only export function resetShadowDomCache() { MapWrapper.clear(_componentUIDs); _nextComponentUID = 0; + MapWrapper.clear(_sharedStyleTexts); } diff --git a/modules/angular2/test/core/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js b/modules/angular2/test/core/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js index 07f1ee7e8d..5bed6536f6 100644 --- a/modules/angular2/test/core/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js +++ b/modules/angular2/test/core/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js @@ -13,7 +13,9 @@ import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; import {ShadowDomStrategy, NativeShadowDomStrategy, - EmulatedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; + EmulatedScopedShadowDomStrategy, + EmulatedUnscopedShadowDomStrategy, +} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {TemplateLoader} from 'angular2/src/core/compiler/template_loader'; import {ComponentUrlMapper} from 'angular2/src/core/compiler/component_url_mapper'; import {UrlResolver} from 'angular2/src/core/compiler/url_resolver'; @@ -35,7 +37,8 @@ export function main() { StringMapWrapper.forEach({ "native" : new NativeShadowDomStrategy(styleUrlResolver), - "emulated" : new EmulatedShadowDomStrategy(styleInliner, styleUrlResolver, DOM.createElement('div')) + "scoped" : new EmulatedScopedShadowDomStrategy(styleInliner, styleUrlResolver, DOM.createElement('div')), + "unscoped" : new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, DOM.createElement('div')), }, (strategy, name) => { diff --git a/modules/angular2/test/core/compiler/shadow_dom_strategy_spec.js b/modules/angular2/test/core/compiler/shadow_dom_strategy_spec.js index 105b62098d..40fab0c814 100644 --- a/modules/angular2/test/core/compiler/shadow_dom_strategy_spec.js +++ b/modules/angular2/test/core/compiler/shadow_dom_strategy_spec.js @@ -2,7 +2,8 @@ import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'a import { NativeShadowDomStrategy, - EmulatedShadowDomStrategy, + EmulatedScopedShadowDomStrategy, + EmulatedUnscopedShadowDomStrategy, resetShadowDomCache, } from 'angular2/src/core/compiler/shadow_dom_strategy'; import {UrlResolver} from 'angular2/src/core/compiler/url_resolver'; @@ -53,7 +54,7 @@ export function main() { }); }); - describe('EmulatedShadowDomStratgey', () => { + describe('EmulatedScopedShadowDomStratgey', () => { var xhr, styleHost; beforeEach(() => { @@ -62,7 +63,7 @@ export function main() { xhr = new FakeXHR(); var styleInliner = new StyleInliner(xhr, styleUrlResolver, urlResolver); styleHost = el('
'); - strategy = new EmulatedShadowDomStrategy(styleInliner, styleUrlResolver, styleHost); + strategy = new EmulatedScopedShadowDomStrategy(styleInliner, styleUrlResolver, styleHost); resetShadowDomCache(); }); @@ -141,6 +142,71 @@ export function main() { expect(DOM.getAttribute(elt, '_nghost-0')).toEqual(''); }); }); + + describe('EmulatedUnscopedShadowDomStratgey', () => { + var styleHost; + + beforeEach(() => { + var urlResolver = new UrlResolver(); + var styleUrlResolver = new StyleUrlResolver(urlResolver); + styleHost = el('
'); + strategy = new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, styleHost); + resetShadowDomCache(); + }); + + it('should attach the view nodes as child of the host element', () => { + var host = el('
original content
'); + var nodes = el('
view
'); + var pv = new ProtoView(nodes, new DynamicProtoChangeDetector(null), null); + var view = pv.instantiate(null, null); + + strategy.attachTemplate(host, view); + var firstChild = DOM.firstChild(host); + expect(DOM.tagName(firstChild)).toEqual('DIV'); + expect(firstChild).toHaveText('view'); + expect(host).toHaveText('view'); + }); + + it('should rewrite style urls', () => { + var css = '.foo {background-image: url("img.jpg");}'; + expect(strategy.transformStyleText(css, 'http://base', null)) + .toEqual(".foo {background-image: url('http://base/img.jpg');}"); + }); + + it('should not inline import rules', () => { + var css = '@import "other.css";'; + expect(strategy.transformStyleText(css, 'http://base', null)) + .toEqual("@import 'http://base/other.css';"); + }); + + it('should move the style element to the style host', () => { + var originalHost = el('
'); + var styleEl = el(''); + DOM.appendChild(originalHost, styleEl); + expect(originalHost).toHaveText('/*css*/'); + + strategy.handleStyleElement(styleEl); + expect(originalHost).toHaveText(''); + expect(styleHost).toHaveText('/*css*/'); + }); + + it('should insert the same style only once in the style host', () => { + var originalHost = el('
'); + var styleEl1 = el(''); + var styleEl2 = el(''); + var styleEl1bis = el(''); + + DOM.appendChild(originalHost, styleEl1); + DOM.appendChild(originalHost, styleEl2); + DOM.appendChild(originalHost, styleEl1bis); + + strategy.handleStyleElement(styleEl1); + strategy.handleStyleElement(styleEl2); + strategy.handleStyleElement(styleEl1bis); + expect(originalHost).toHaveText(''); + expect(styleHost).toHaveText('/*css 1*//*css 2*/'); + }); + }); } class FakeXHR extends XHR { diff --git a/modules/angular2/test/core/compiler/view_spec.js b/modules/angular2/test/core/compiler/view_spec.js index 762f23279a..978ef67721 100644 --- a/modules/angular2/test/core/compiler/view_spec.js +++ b/modules/angular2/test/core/compiler/view_spec.js @@ -1,7 +1,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el, proxy} from 'angular2/test_lib'; import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'angular2/src/core/compiler/view'; import {ProtoElementInjector, ElementInjector, DirectiveBinding} from 'angular2/src/core/compiler/element_injector'; -import {EmulatedShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {EmulatedScopedShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; import {Component, Decorator, Viewport, Directive, onChange} from 'angular2/src/core/annotations/annotations'; import {Lexer, Parser, DynamicProtoChangeDetector, @@ -396,7 +396,7 @@ export function main() { new DynamicProtoChangeDetector(null), null); var pv = new ProtoView(el(''), - new DynamicProtoChangeDetector(null), new EmulatedShadowDomStrategy(null, null, null)); + new DynamicProtoChangeDetector(null), new EmulatedScopedShadowDomStrategy(null, null, null)); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true)); binder.componentDirective = new DirectiveMetadataReader().read(SomeComponent); binder.nestedProtoView = subpv;