From d0ca07afaade5fa1956b8dcf27d79091024d0d5f Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 19 Feb 2015 11:55:43 +0100 Subject: [PATCH] refactor(Compiler): introduce ShimComponent to shim CSS & DOM in emulated mode Closes #715 --- .../core/compiler/pipeline/default_steps.js | 14 +- ..._dom_transformer.js => shim_shadow_css.js} | 39 +- .../core/compiler/pipeline/shim_shadow_dom.js | 38 ++ .../shadow_dom_emulation/shim_component.js | 99 ++++ .../compiler/shadow_dom_emulation/shim_css.js | 431 ------------------ .../src/core/compiler/shadow_dom_strategy.js | 19 +- modules/angular2/src/core/compiler/view.js | 5 +- .../pipeline/shadow_dom_transformer_spec.js | 128 ------ .../compiler/pipeline/shim_shadow_css_spec.js | 91 ++++ .../compiler/pipeline/shim_shadow_dom_spec.js | 97 ++++ .../shadow_dom/shim_component_spec.js | 128 ++++++ .../core/compiler/shadow_dom/shim_css_spec.js | 102 ----- 12 files changed, 486 insertions(+), 705 deletions(-) rename modules/angular2/src/core/compiler/pipeline/{shadow_dom_transformer.js => shim_shadow_css.js} (54%) create mode 100644 modules/angular2/src/core/compiler/pipeline/shim_shadow_dom.js create mode 100644 modules/angular2/src/core/compiler/shadow_dom_emulation/shim_component.js delete mode 100644 modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js delete mode 100644 modules/angular2/test/core/compiler/pipeline/shadow_dom_transformer_spec.js create mode 100644 modules/angular2/test/core/compiler/pipeline/shim_shadow_css_spec.js create mode 100644 modules/angular2/test/core/compiler/pipeline/shim_shadow_dom_spec.js create mode 100644 modules/angular2/test/core/compiler/shadow_dom/shim_component_spec.js delete mode 100644 modules/angular2/test/core/compiler/shadow_dom/shim_css_spec.js diff --git a/modules/angular2/src/core/compiler/pipeline/default_steps.js b/modules/angular2/src/core/compiler/pipeline/default_steps.js index e841c1a38c..16b180d07c 100644 --- a/modules/angular2/src/core/compiler/pipeline/default_steps.js +++ b/modules/angular2/src/core/compiler/pipeline/default_steps.js @@ -9,9 +9,10 @@ import {ElementBindingMarker} from './element_binding_marker'; import {ProtoViewBuilder} from './proto_view_builder'; import {ProtoElementInjectorBuilder} from './proto_element_injector_builder'; import {ElementBinderBuilder} from './element_binder_builder'; -import {ShadowDomTransformer} from './shadow_dom_transformer'; +import {ShimShadowCss} from './shim_shadow_css'; +import {ShimShadowDom} from './shim_shadow_dom'; import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; -import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; +import {ShadowDomStrategy, EmulatedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {stringify} from 'angular2/src/facade/lang'; import {DOM} from 'angular2/src/facade/dom'; @@ -31,8 +32,8 @@ export function createDefaultSteps( var steps = [new ViewSplitter(parser, compilationUnit)]; - if (!(shadowDomStrategy instanceof NativeShadowDomStrategy)) { - var step = new ShadowDomTransformer(compiledComponent, shadowDomStrategy, DOM.defaultDoc().head); + if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) { + var step = new ShimShadowCss(compiledComponent, shadowDomStrategy, DOM.defaultDoc().head); ListWrapper.push(steps, step); } @@ -46,5 +47,10 @@ export function createDefaultSteps( new ElementBinderBuilder(parser, compilationUnit) ]); + if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) { + var step = new ShimShadowDom(compiledComponent, shadowDomStrategy); + ListWrapper.push(steps, step); + } + return steps; } diff --git a/modules/angular2/src/core/compiler/pipeline/shadow_dom_transformer.js b/modules/angular2/src/core/compiler/pipeline/shim_shadow_css.js similarity index 54% rename from modules/angular2/src/core/compiler/pipeline/shadow_dom_transformer.js rename to modules/angular2/src/core/compiler/pipeline/shim_shadow_css.js index 7d90dd0d51..655de6b4c9 100644 --- a/modules/angular2/src/core/compiler/pipeline/shadow_dom_transformer.js +++ b/modules/angular2/src/core/compiler/pipeline/shim_shadow_css.js @@ -4,24 +4,20 @@ import {CompileControl} from './compile_control'; import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; -import {shimCssText} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_css'; import {DOM, Element} from 'angular2/src/facade/dom'; -import {isPresent, isBlank} from 'angular2/src/facade/lang'; -import {StringMapWrapper} from 'angular2/src/facade/collection'; +import {isPresent, isBlank, Type} from 'angular2/src/facade/lang'; -var _cssCache = StringMapWrapper.create(); - -export class ShadowDomTransformer extends CompileStep { - _selector: string; +export class ShimShadowCss extends CompileStep { _strategy: ShadowDomStrategy; _styleHost: Element; _lastInsertedStyle: Element; + _component: Type; constructor(cmpMetadata: DirectiveMetadata, strategy: ShadowDomStrategy, styleHost: Element) { super(); this._strategy = strategy; - this._selector = cmpMetadata.annotation.selector; + this._component = cmpMetadata.type; this._styleHost = styleHost; this._lastInsertedStyle = null; } @@ -33,34 +29,13 @@ export class ShadowDomTransformer extends CompileStep { if (this._strategy.extractStyles()) { DOM.remove(current.element); var css = DOM.getText(current.element); - if (this._strategy.shim()) { - // The css generated here is unique for the component (because of the shim). - // Then we do not need to cache it. - css = shimCssText(css, this._selector); - this._insertStyle(this._styleHost, css); - } else { - var seen = isPresent(StringMapWrapper.get(_cssCache, css)); - if (!seen) { - StringMapWrapper.set(_cssCache, css, true); - this._insertStyle(this._styleHost, css); - } - } - } - } else { - if (this._strategy.shim()) { - try { - DOM.setAttribute(current.element, this._selector, ''); - } catch(e) { - // TODO(vicb): for now only simple selector (tag name) are supported - } + var shimComponent = this._strategy.getShimComponent(this._component); + css = shimComponent.shimCssText(css); + this._insertStyle(this._styleHost, css); } } } - clearCache() { - _cssCache = StringMapWrapper.create(); - } - _insertStyle(el: Element, css: string) { var style = DOM.createStyleElement(css); if (isBlank(this._lastInsertedStyle)) { diff --git a/modules/angular2/src/core/compiler/pipeline/shim_shadow_dom.js b/modules/angular2/src/core/compiler/pipeline/shim_shadow_dom.js new file mode 100644 index 0000000000..17eef7d416 --- /dev/null +++ b/modules/angular2/src/core/compiler/pipeline/shim_shadow_dom.js @@ -0,0 +1,38 @@ +import {CompileStep} from './compile_step'; +import {CompileElement} from './compile_element'; +import {CompileControl} from './compile_control'; + +import {isPresent} from 'angular2/src/facade/lang'; + +import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; +import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; + +import {ShimComponent} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component'; + +export class ShimShadowDom extends CompileStep { + _strategy: ShadowDomStrategy; + _shimComponent: ShimComponent; + + constructor(cmpMetadata: DirectiveMetadata, strategy: ShadowDomStrategy) { + super(); + this._strategy = strategy; + this._shimComponent = strategy.getShimComponent(cmpMetadata.type); + } + + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + if (current.ignoreBindings) { + return; + } + + // Shim the element as a child of the compiled component + this._shimComponent.shimContentElement(current.element); + + // If the current element is also a component, shim it as a host + var host = current.componentDirective; + if (isPresent(host)) { + var shimComponent = this._strategy.getShimComponent(host.type); + shimComponent.shimHostElement(current.element); + } + } +} + diff --git a/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_component.js b/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_component.js new file mode 100644 index 0000000000..7f7b0566ce --- /dev/null +++ b/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_component.js @@ -0,0 +1,99 @@ +import {Element, DOM} from 'angular2/src/facade/dom'; +import {Map, MapWrapper} from 'angular2/src/facade/collection'; +import {int, isBlank, Type} from 'angular2/src/facade/lang'; + +import {ShadowCss} from './shadow_css'; + +/** + * Used to shim component CSS & DOM + */ +export class ShimComponent { + constructor(component: Type) { + } + + shimCssText(cssText: string): string { + return null + } + + shimContentElement(element: Element) {} + + shimHostElement(element: Element) {} +} + +/** + * Native components does not need to the shim. + * + * All methods are no-ops. + */ +export class ShimNativeComponent extends ShimComponent { + constructor(component: Type) { + super(component); + }; + + shimCssText(cssText: string): string { + return cssText; + } + + shimContentElement(element: Element) { + } + + shimHostElement(element: Element) { + } +} + +var _componentCache: Map = MapWrapper.create(); +var _componentId: int = 0; + +// Reset the component cache - used for tests only +export function resetShimComponentCache() { + MapWrapper.clear(_componentCache); + _componentId = 0; +} + +/** + * Emulated components need to be shimmed: + * - An attribute needs to be added to the host, + * - An attribute needs to be added to all nodes in their content, + * - The CSS needs to be scoped. + */ +export class ShimEmulatedComponent extends ShimComponent { + _cmpId: int; + + constructor(component: Type) { + super(component); + + // Generates a unique ID for components + var componentId = MapWrapper.get(_componentCache, component); + if (isBlank(componentId)) { + componentId = _componentId++; + MapWrapper.set(_componentCache, component, componentId); + } + this._cmpId = componentId; + }; + + // Scope the CSS + shimCssText(cssText: string): string { + var shadowCss = new ShadowCss(); + return shadowCss.shimCssText(cssText, this._getContentAttribute(), this._getHostAttribute()); + } + + // Add an attribute on a content element + shimContentElement(element: Element) { + DOM.setAttribute(element, this._getContentAttribute(), ''); + } + + // Add an attribute to the host + shimHostElement(element: Element) { + DOM.setAttribute(element, this._getHostAttribute(), ''); + } + + // Return the attribute to be added to the component + _getHostAttribute() { + return `_nghost-${this._cmpId}`; + } + + // Returns the attribute to be added on every single nodes in the component + _getContentAttribute() { + return `_ngcontent-${this._cmpId}`; + } +} diff --git a/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js b/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js deleted file mode 100644 index 7e4f0af870..0000000000 --- a/modules/angular2/src/core/compiler/shadow_dom_emulation/shim_css.js +++ /dev/null @@ -1,431 +0,0 @@ -import {StringWrapper, RegExpWrapper, isPresent, BaseException, int} from 'angular2/src/facade/lang'; -import {List, ListWrapper} from 'angular2/src/facade/collection'; - -export function shimCssText(css: string, tag: string) { - return new CssShim(tag).shimCssText(css); -} - -var _HOST_RE = RegExpWrapper.create(':host', 'i'); -var _HOST_TOKEN = '-host-element'; -var _HOST_TOKEN_RE = RegExpWrapper.create('-host-element'); -var _PAREN_SUFFIX = ')(?:\\((' + - '(?:\\([^)(]*\\)|[^)(]*)+?' + - ')\\))?([^,{]*)'; -var _COLON_HOST_RE = RegExpWrapper.create(`(${_HOST_TOKEN}${_PAREN_SUFFIX}`, 'im'); - -var _POLYFILL_NON_STRICT = 'polyfill-non-strict'; -var _POLYFILL_UNSCOPED_NEXT_SELECTOR = 'polyfill-unscoped-next-selector'; -var _POLYFILL_NEXT_SELECTOR = 'polyfill-next-selector'; -var _CONTENT_RE = RegExpWrapper.create('[^}]*content:[\\s]*[\'"](.*?)[\'"][;\\s]*[^}]*}', 'im'); -var _COMBINATORS = [ - RegExpWrapper.create('/shadow/', 'i'), - RegExpWrapper.create('/shadow-deep/', 'i'), - RegExpWrapper.create('::shadow', 'i'), - RegExpWrapper.create('/deep/', 'i'), -]; -var _COLON_SELECTORS = RegExpWrapper.create('(' + _HOST_TOKEN + ')(\\(.*\\))?(.*)', 'i'); -var _SELECTOR_SPLITS = [' ', '>', '+', '~']; -var _SIMPLE_SELECTORS = RegExpWrapper.create('([^:]*)(:*)(.*)', 'i'); -var _IS_SELECTORS = RegExpWrapper.create('\\[is=[\'"]([^\\]]*)[\'"]\\]', 'i'); - -var _$EOF = 0; -var _$LBRACE = 123; -var _$RBRACE = 125; -var _$TAB = 9; -var _$SPACE = 32; -var _$NBSP = 160; - -export class CssShim { - _tag: string; - _attr: string; - - constructor(tag: string) { - this._tag = tag; - this._attr = `[${tag}]`; - } - - shimCssText(css: string): string { - var preprocessed = this.convertColonHost(css); - var rules = this.cssToRules(preprocessed); - return this.scopeRules(rules); - } - - convertColonHost(css: string):string { - css = StringWrapper.replaceAll(css, _HOST_RE, _HOST_TOKEN); - - var partReplacer = function(host, part, suffix) { - part = StringWrapper.replaceAll(part, _HOST_TOKEN_RE, ''); - return `${host}${part}${suffix}`; - } - - return StringWrapper.replaceAllMapped(css, _COLON_HOST_RE, function(m) { - var base = _HOST_TOKEN; - var inParens = m[2]; - var rest = m[3]; - - if (isPresent(inParens)) { - var srcParts = inParens.split(','); - var dstParts = []; - - for (var i = 0; i < srcParts.length; i++) { - var part = srcParts[i].trim(); - if (part.length > 0) { - ListWrapper.push(dstParts, partReplacer(base, part, rest)); - } - } - - return ListWrapper.join(dstParts, ','); - } else { - return `${base}${rest}`; - } - }); - } - - cssToRules(css: string): List<_Rule> { - return new _Parser(css).parse(); - } - - scopeRules(rules: List<_Rule>): string { - var scopedRules = []; - var prevRule = null; - - for (var i = 0; i < rules.length; i++) { - var rule = rules[i]; - if (isPresent(prevRule) && - prevRule.selectorText == _POLYFILL_NON_STRICT) { - ListWrapper.push(scopedRules, this.scopeNonStrictMode(rule)); - - } else if (isPresent(prevRule) && - prevRule.selectorText == _POLYFILL_UNSCOPED_NEXT_SELECTOR) { - var content = this.extractContent(prevRule); - var r = new _Rule(content, rule.body, null); - ListWrapper.push(scopedRules, this.ruleToString(r)); - - } else if (isPresent(prevRule) && - prevRule.selectorText == _POLYFILL_NEXT_SELECTOR) { - - var content = this.extractContent(prevRule); - var r = new _Rule(content, rule.body, null); - ListWrapper.push(scopedRules, this.scopeStrictMode(r)) - - } else if (rule.selectorText != _POLYFILL_NON_STRICT && - rule.selectorText != _POLYFILL_UNSCOPED_NEXT_SELECTOR && - rule.selectorText != _POLYFILL_NEXT_SELECTOR) { - ListWrapper.push(scopedRules, this.scopeStrictMode(rule)); - } - prevRule = rule; - } - - return ListWrapper.join(scopedRules, '\n'); - } - - extractContent(rule: _Rule): string { - var match = RegExpWrapper.firstMatch(_CONTENT_RE, rule.body); - return isPresent(match) ? match[1] : ''; - } - - ruleToString(rule: _Rule): string { - return `${rule.selectorText} ${rule.body}`; - } - - scopeStrictMode(rule: _Rule): string { - if (rule.hasNestedRules()) { - var selector = rule.selectorText; - var rules = this.scopeRules(rule.rules); - return `${selector} {\n${rules}\n}`; - } - - var scopedSelector = this.scopeSelector(rule.selectorText, true); - var scopedBody = rule.body; - return `${scopedSelector} ${scopedBody}`; - } - - scopeNonStrictMode(rule: _Rule): string { - var scopedSelector = this.scopeSelector(rule.selectorText, false); - var scopedBody = rule.body; - return `${scopedSelector} ${scopedBody}`; - } - - scopeSelector(selector: string, strict: boolean) { - var parts = this.replaceCombinators(selector).split(','); - var scopedParts = []; - for (var i = 0; i < parts.length; i++) { - var part = parts[i]; - var sel = this.scopeSimpleSelector(part.trim(), strict); - ListWrapper.push(scopedParts, sel) - } - return ListWrapper.join(scopedParts, ', '); - } - - replaceCombinators(selector: string): string { - for (var i = 0; i < _COMBINATORS.length; i++) { - var combinator = _COMBINATORS[i]; - selector = StringWrapper.replaceAll(selector, combinator, ''); - } - - return selector; - } - - scopeSimpleSelector(selector: string, strict: boolean) { - if (StringWrapper.contains(selector, _HOST_TOKEN)) { - return this.replaceColonSelectors(selector); - } else if (strict) { - return this.insertTagToEverySelectorPart(selector); - } else { - return `${this._tag} ${selector}`; - } - } - - replaceColonSelectors(css: string): string { - return StringWrapper.replaceAllMapped(css, _COLON_SELECTORS, (m) => { - var selectorInParens; - if (isPresent(m[2])) { - var len = selectorInParens.length; - selectorInParens = StringWrapper.substring(selectorInParens, 1, len - 1); - } else { - selectorInParens = ''; - } - var rest = m[3]; - return `${this._tag}${selectorInParens}${rest}`; - }); - } - - insertTagToEverySelectorPart(selector: string): string { - selector = this.handleIsSelector(selector); - - for (var i = 0; i < _SELECTOR_SPLITS.length; i++) { - var split = _SELECTOR_SPLITS[i]; - var parts = selector.split(split); - for (var j = 0; j < parts.length; j++) { - parts[j] = this.insertAttrSuffixIntoSelectorPart(parts[j].trim()); - } - selector = parts.join(split); - } - return selector; - } - - insertAttrSuffixIntoSelectorPart(p: string): string { - var shouldInsert = p.length > 0 && - !ListWrapper.contains(_SELECTOR_SPLITS, p) && - !StringWrapper.contains(p, this._attr); - return shouldInsert ? this.insertAttr(p) : p; - } - - insertAttr(selector: string): string { - return StringWrapper.replaceAllMapped(selector, _SIMPLE_SELECTORS, (m) => { - var basePart = m[1]; - var colonPart = m[2]; - var rest = m[3]; - return (m[0].length > 0) ? `${basePart}${this._attr}${colonPart}${rest}` : ''; - }); - } - - handleIsSelector(selector: string) { - return StringWrapper.replaceAllMapped(selector, _IS_SELECTORS, function(m) { - return m[1]; - }); - } -} - -class _Token { - string: string; - type: string; - - constructor(string: string, type: string) { - this.string = string; - this.type = type; - } -} - -var _EOF_TOKEN = new _Token(null, null); - -class _Lexer { - peek: int; - index: int; - input: string; - length: int; - - constructor(input: string) { - this.input = input; - this.length = input.length; - this.index = -1; - this.advance(); - } - - parse(): List<_Token> { - var tokens = []; - var token = this.scanToken(); - while (token !== _EOF_TOKEN) { - ListWrapper.push(tokens, token); - token = this.scanToken(); - } - return tokens; - } - - scanToken(): _Token { - this.skipWhitespace(); - if (this.peek === _$EOF) return _EOF_TOKEN; - if (this.isBodyEnd(this.peek)) { - this.advance(); - return new _Token('}', 'rparen'); - } - if (this.isMedia(this.peek)) return this.scanMedia(); - if (this.isSelector(this.peek)) return this.scanSelector(); - if (this.isBodyStart(this.peek)) return this.scanBody(); - - return _EOF_TOKEN; - } - - isSelector(v: int): boolean { - return !this.isBodyStart(v) && v !== _$EOF; - } - - isBodyStart(v: int): boolean { - return v === _$LBRACE; - } - - isBodyEnd(v: int): boolean { - return v === _$RBRACE; - } - - isMedia(v: int): boolean { - return v === 64; // @ -> 64 - } - - isWhitespace(v: int): boolean { - return (v >= _$TAB && v <= _$SPACE) || (v == _$NBSP) - } - - skipWhitespace() { - while (this.isWhitespace(this.peek)) { - if (++this.index >= this.length) { - this.peek = _$EOF; - return; - } else { - this.peek = StringWrapper.charCodeAt(this.input, this.index); - } - } - } - - scanSelector(): _Token { - var start = this.index; - this.advance(); - while (this.isSelector(this.peek)) { - this.advance(); - } - var selector = StringWrapper.substring(this.input, start, this.index); - return new _Token(selector.trim(), 'selector'); - } - - scanBody(): _Token { - var start = this.index; - this.advance(); - while (!this.isBodyEnd(this.peek)) { - this.advance(); - } - this.advance(); - var body = StringWrapper.substring(this.input, start, this.index); - return new _Token(body, 'body'); - } - - scanMedia(): _Token { - var start = this.index; - this.advance(); - while (!this.isBodyStart(this.peek)) { - this.advance(); - } - var media = StringWrapper.substring(this.input, start, this.index); - this.advance(); // skip "{" - return new _Token(media, 'media'); - } - - advance() { - this.index++; - if (this.index >= this.length) { - this.peek = _$EOF; - } else { - this.peek = StringWrapper.charCodeAt(this.input, this.index); - } - } -} - -class _Parser { - tokens: List<_Token>; - currentIndex: int; - - constructor(input: string) { - this.tokens = new _Lexer(input).parse(); - this.currentIndex = -1; - } - - parse(): List<_Rule> { - var rules = []; - var rule; - while (isPresent(rule = this.parseRule())) { - ListWrapper.push(rules, rule); - } - return rules; - } - - parseRule(): _Rule { - try { - if (this.getNext().type === 'media') { - return this.parseMedia(); - } else { - return this.parseCssRule(); - } - } catch (e) { - return null; - } - } - - parseMedia(): _Rule { - this.advance('media'); - var media = this.getCurrent().string; - var rules = []; - while (this.getNext().type !== 'rparen') { - ListWrapper.push(rules, this.parseCssRule()); - } - this.advance('rparen'); - return new _Rule(media.trim(), null, rules); - } - - parseCssRule() { - this.advance('selector'); - var selector = this.getCurrent().string; - this.advance('body'); - var body = this.getCurrent().string; - return new _Rule(selector, body, null); - } - - advance(expected: string) { - this.currentIndex++; - if (this.getCurrent().type !== expected) { - throw new BaseException(`Unexpected token "${this.getCurrent().type}". Expected "${expected}"`); - } - } - - getNext(): _Token { - return this.tokens[this.currentIndex + 1]; - } - - getCurrent(): _Token { - return this.tokens[this.currentIndex]; - } -} - -export class _Rule { - selectorText: string; - body: string; - rules: List<_Rule>; - - constructor(selectorText: string, body: string, rules: List<_Rule>) { - this.selectorText = selectorText; - this.body = body; - this.rules = rules; - } - - hasNestedRules() { - return isPresent(this.rules); - } -} diff --git a/modules/angular2/src/core/compiler/shadow_dom_strategy.js b/modules/angular2/src/core/compiler/shadow_dom_strategy.js index 90049e2954..9949659b43 100644 --- a/modules/angular2/src/core/compiler/shadow_dom_strategy.js +++ b/modules/angular2/src/core/compiler/shadow_dom_strategy.js @@ -1,16 +1,21 @@ import {Type, isBlank, isPresent} from 'angular2/src/facade/lang'; import {DOM, Element} from 'angular2/src/facade/dom'; import {List, ListWrapper} from 'angular2/src/facade/collection'; + import {View} from './view'; + import {Content} from './shadow_dom_emulation/content_tag'; import {LightDom} from './shadow_dom_emulation/light_dom'; +import {ShimComponent, ShimEmulatedComponent, ShimNativeComponent} from './shadow_dom_emulation/shim_component'; export class ShadowDomStrategy { attachTemplate(el:Element, view:View){} constructLightDom(lightDomView:View, shadowDomView:View, el:Element){} polyfillDirectives():List{ return null; } - shim(): boolean { return false; } extractStyles(): boolean { return false; } + getShimComponent(component: Type): ShimComponent { + return null; + } } export class EmulatedShadowDomStrategy extends ShadowDomStrategy { @@ -31,12 +36,12 @@ export class EmulatedShadowDomStrategy extends ShadowDomStrategy { return [Content]; } - shim(): boolean { + extractStyles(): boolean { return true; } - extractStyles(): boolean { - return true; + getShimComponent(component: Type): ShimComponent { + return new ShimEmulatedComponent(component); } } @@ -57,12 +62,12 @@ export class NativeShadowDomStrategy extends ShadowDomStrategy { return []; } - shim(): boolean { + extractStyles(): boolean { return false; } - extractStyles(): boolean { - return false; + getShimComponent(component: Type): ShimComponent { + return new ShimNativeComponent(component); } } diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index b39ce3743d..10d683ea07 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -535,12 +535,15 @@ export class ProtoView { ): ProtoView { DOM.addClass(insertionElement, NG_BINDING_CLASS); + var cmpType = rootComponentAnnotatedType.type; var rootProtoView = new ProtoView(insertionElement, protoChangeDetector, shadowDomStrategy); rootProtoView.instantiateInPlace = true; var binder = rootProtoView.bindElement( - new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true)); + new ProtoElementInjector(null, 0, [cmpType], true)); binder.componentDirective = rootComponentAnnotatedType; binder.nestedProtoView = protoView; + var shimComponent = shadowDomStrategy.getShimComponent(cmpType); + shimComponent.shimHostElement(insertionElement); return rootProtoView; } } diff --git a/modules/angular2/test/core/compiler/pipeline/shadow_dom_transformer_spec.js b/modules/angular2/test/core/compiler/pipeline/shadow_dom_transformer_spec.js deleted file mode 100644 index 892097793f..0000000000 --- a/modules/angular2/test/core/compiler/pipeline/shadow_dom_transformer_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib'; - -import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline'; -import {ShadowDomTransformer} from 'angular2/src/core/compiler/pipeline/shadow_dom_transformer'; -import {Component} from 'angular2/src/core/annotations/annotations'; -import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; -import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; -import {shimCssText} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_css'; - -import {DOM} from 'angular2/src/facade/dom'; -import {MapWrapper} from 'angular2/src/facade/collection'; - -export function main() { - describe('ShadowDomTransformer', () => { - function createPipeline(selector, strategy:ShadowDomStrategy, styleHost) { - var component = new Component({selector: selector}); - var meta = new DirectiveMetadata(null, component); - var pipe = new ShadowDomTransformer(meta, strategy, styleHost); - pipe.clearCache(); - return new CompilePipeline([pipe]); - } - - it('it should set ignoreBindings to true for style elements', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(false, false), host); - var results = pipeline.process(el('
')); - expect(results[0].ignoreBindings).toBe(false); - expect(results[1].ignoreBindings).toBe(true); - }); - - describe('css', () => { - it('should not extract the styles when extractStyles() is false', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(false, false), host); - var template = el(''); - pipeline.process(template); - expect(template).toHaveText('.s{}'); - }); - - it('should move the styles to the host when extractStyles() is true', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(true, false), host); - var template = el('
'); - pipeline.process(template); - expect(template).toHaveText(''); - expect(host).toHaveText('.s{}'); - }); - - it('should preserve original content when moving styles', () => { - var host = el('
original content
'); - var pipeline = createPipeline('foo', new FakeStrategy(true, false), host); - var template = el('
'); - pipeline.process(template); - expect(template).toHaveText(''); - expect(host).toHaveText('.s{}original content'); - }); - - it('should move the styles to the host in the original order', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(true, false), host); - var template = el('
'); - pipeline.process(template); - expect(host).toHaveText('.s1{}.s2{}'); - }); - - it('should shim the styles when shim() and extractStyles() are true', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(true, true), host); - var template = el('
'); - pipeline.process(template); - expect(host).toHaveText(shimCssText('.s1{}', 'foo')); - }); - - it('should deduplicate styles before moving them when shim() is false', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(true, false), host); - var template = el('
'); - pipeline.process(template); - expect(host).toHaveText('.s1{}'); - }); - }); - - describe('html', () => { - it('should add an attribute to all children when shim() is true', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(false, true), host); - var template = el('
'); - pipeline.process(template); - expect(DOM.getOuterHTML(template)).toEqual('
') - }); - - it('should not modify the template when shim() is false', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo', new FakeStrategy(false, false), host); - var template = el('
'); - pipeline.process(template); - expect(DOM.getOuterHTML(template)).toEqual('
') - }); - - it('should not throw with complex selectors', () => { - var host = DOM.createElement('div'); - var pipeline = createPipeline('foo[bar]', new FakeStrategy(false, true), host); - var template = el('
'); - expect(() => pipeline.process(template)).not.toThrow(); - }); - - }); - }); -} - -class FakeStrategy extends ShadowDomStrategy { - _extractStyles: boolean; - _shim: boolean; - - constructor(extractStyles: boolean, shim: boolean) { - super(); - this._extractStyles = extractStyles; - this._shim = shim; - } - - extractStyles(): boolean { - return this._extractStyles; - } - - shim(): boolean { - return this._shim; - } -} diff --git a/modules/angular2/test/core/compiler/pipeline/shim_shadow_css_spec.js b/modules/angular2/test/core/compiler/pipeline/shim_shadow_css_spec.js new file mode 100644 index 0000000000..c67bb14bd3 --- /dev/null +++ b/modules/angular2/test/core/compiler/pipeline/shim_shadow_css_spec.js @@ -0,0 +1,91 @@ +import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib'; + +import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline'; +import {ShimShadowCss} from 'angular2/src/core/compiler/pipeline/shim_shadow_css'; +import {ShimComponent} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component'; + +import {Component} from 'angular2/src/core/annotations/annotations'; +import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; +import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; + +import {Type} from 'angular2/src/facade/lang'; + +export function main() { + describe('ShimShadowCss', () => { + function createPipeline(strategy:ShadowDomStrategy, styleHost) { + var component = new Component({selector: 'selector'}); + var meta = new DirectiveMetadata(null, component); + var shimShadowCss = new ShimShadowCss(meta, strategy, styleHost); + return new CompilePipeline([shimShadowCss]); + } + + it('it should set ignoreBindings to true for style elements', () => { + var host = el('
'); + var pipeline = createPipeline(new FakeStrategy(false), host); + var results = pipeline.process(el('
')); + expect(results[0].ignoreBindings).toBe(false); + expect(results[1].ignoreBindings).toBe(true); + }); + + it('should not extract the styles when extractStyles() is false', () => { + var host = el('
'); + var pipeline = createPipeline(new FakeStrategy(false), host); + var template = el(''); + pipeline.process(template); + expect(template).toHaveText('.s{}'); + }); + + it('should move the styles to the host when extractStyles() is true', () => { + var host = el('
'); + var pipeline = createPipeline(new FakeStrategy(true), host); + var template = el('
'); + pipeline.process(template); + expect(template).toHaveText(''); + expect(host).toHaveText('/* shim */.s{}'); + }); + + it('should preserve original content when moving styles', () => { + var host = el('
original content
'); + var pipeline = createPipeline(new FakeStrategy(true), host); + var template = el('
'); + pipeline.process(template); + expect(template).toHaveText(''); + expect(host).toHaveText('/* shim */.s{}original content'); + }); + + it('should move the styles to the host in the original order', () => { + var host = el('
'); + var pipeline = createPipeline(new FakeStrategy(true), host); + var template = el('
'); + pipeline.process(template); + expect(host).toHaveText('/* shim */.s1{}/* shim */.s2{}'); + }); + }); +} + +class FakeStrategy extends ShadowDomStrategy { + _extractStyles: boolean; + + constructor(extractStyles: boolean) { + super(); + this._extractStyles = extractStyles; + } + + extractStyles(): boolean { + return this._extractStyles; + } + + getShimComponent(component: Type): ShimComponent { + return new FakeShimComponent(component); + } +} + +class FakeShimComponent extends ShimComponent { + constructor(component: Type) { + super(component); + } + + shimCssText(cssText: string): string { + return '/* shim */' + cssText; + } +} diff --git a/modules/angular2/test/core/compiler/pipeline/shim_shadow_dom_spec.js b/modules/angular2/test/core/compiler/pipeline/shim_shadow_dom_spec.js new file mode 100644 index 0000000000..b61b64c1ba --- /dev/null +++ b/modules/angular2/test/core/compiler/pipeline/shim_shadow_dom_spec.js @@ -0,0 +1,97 @@ +import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/test_lib'; + +import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline'; +import {ShimShadowDom} from 'angular2/src/core/compiler/pipeline/shim_shadow_dom'; +import {CompileElement} from 'angular2/src/core/compiler/pipeline/compile_element'; +import {CompileStep} from 'angular2/src/core/compiler/pipeline/compile_step'; +import {CompileControl} from 'angular2/src/core/compiler/pipeline/compile_control'; +import {ShimComponent} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component'; + +import {Component} from 'angular2/src/core/annotations/annotations'; +import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; +import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; + +import {Type, isBlank} from 'angular2/src/facade/lang'; +import {DOM, Element} from 'angular2/src/facade/dom'; + +export function main() { + describe('ShimShadowDom', () => { + function createPipeline(ignoreBindings: boolean) { + var component = new Component({selector: 'selector'}); + var meta = new DirectiveMetadata(null, component); + var shimShadowDom = new ShimShadowDom(meta, new FakeStrategy()); + + return new CompilePipeline([ + new MockStep((parent, current, control) => { + current.ignoreBindings = ignoreBindings; + }), + new MockStep((parent, current, control) => { + var el = current.element; + if (DOM.hasClass(el, 'host')) { + current.componentDirective = new DirectiveMetadata(SomeComponent, null); + } + }), + shimShadowDom + ]); + } + + it('should add the content attribute to content element', () => { + var pipeline = createPipeline(false); + var results = pipeline.process(el('
')); + expect(DOM.getAttribute(results[0].element, '_ngcontent')).toEqual('content'); + expect(isBlank(DOM.getAttribute(results[0].element, '_nghost'))).toBeTruthy(); + }); + + it('should add both the content and host attributes to host element', () => { + var pipeline = createPipeline(false); + var results = pipeline.process(el('
')); + expect(DOM.getAttribute(results[0].element, '_ngcontent')).toEqual('content'); + expect(DOM.getAttribute(results[0].element, '_nghost')).toEqual('host'); + }); + + it('should do nothing when ignoreBindings is true', () => { + var pipeline = createPipeline(true); + var results = pipeline.process(el('
')); + expect(isBlank(DOM.getAttribute(results[0].element, '_ngcontent'))).toBeTruthy(); + expect(isBlank(DOM.getAttribute(results[0].element, '_nghost'))).toBeTruthy(); + }); + }); +} + +class FakeStrategy extends ShadowDomStrategy { + constructor() { + super(); + } + + getShimComponent(component: Type): ShimComponent { + return new FakeShimComponent(component); + } +} + +class FakeShimComponent extends ShimComponent { + constructor(component: Type) { + super(component); + } + + shimContentElement(element: Element) { + DOM.setAttribute(element, '_ngcontent', 'content'); + } + + shimHostElement(element: Element) { + DOM.setAttribute(element, '_nghost', 'host'); + } +} + +class MockStep extends CompileStep { + processClosure:Function; + constructor(process) { + super(); + this.processClosure = process; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { + this.processClosure(parent, current, control); + } +} + +class SomeComponent {} + diff --git a/modules/angular2/test/core/compiler/shadow_dom/shim_component_spec.js b/modules/angular2/test/core/compiler/shadow_dom/shim_component_spec.js new file mode 100644 index 0000000000..6d5c25a580 --- /dev/null +++ b/modules/angular2/test/core/compiler/shadow_dom/shim_component_spec.js @@ -0,0 +1,128 @@ +import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'angular2/test_lib'; + +import { + ShimNativeComponent, + ShimEmulatedComponent, + resetShimComponentCache +} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_component'; + +import {ShadowCss} from 'angular2/src/core/compiler/shadow_dom_emulation/shadow_css'; + +import {Type} from 'angular2/src/facade/lang'; +import {DOM} from 'angular2/src/facade/dom'; + +export function main() { + describe('ShimComponent', () => { + + describe('ShimNativeComponent', () => { + function createShim(component: Type) { + return new ShimNativeComponent(component); + } + + it('should not transform the CSS', () => { + var css = '.foo {color: blue;} :host{color: red;}'; + var shim = createShim(SomeComponent); + var shimCss = shim.shimCssText(css); + expect(css).toEqual(shimCss); + }); + + it('should not transform content elements', () => { + var html = '

foo

'; + var element = el(html); + var shim = createShim(SomeComponent); + shim.shimContentElement(element); + expect(DOM.getOuterHTML(element)).toEqual(html); + }); + + it('should not transform host elements', () => { + var html = '

foo

'; + var element = el(html); + var shim = createShim(SomeComponent); + shim.shimHostElement(element); + expect(DOM.getOuterHTML(element)).toEqual(html); + }); + }); + + describe('ShimEmulatedComponent', () => { + beforeEach(() => { + resetShimComponentCache(); + }); + + function createShim(component: Type) { + return new ShimEmulatedComponent(component); + } + + it('should transform the CSS', () => { + var css = '.foo {color: blue;} :host{color: red;}'; + var shim = createShim(SomeComponent); + var shimCss = shim.shimCssText(css); + expect(shimCss).not.toEqual(css); + var shadowCss = new ShadowCss(); + expect(shimCss).toEqual(shadowCss.shimCssText(css, '_ngcontent-0', '_nghost-0')); + }); + + it('should transform content elements', () => { + var html = '

foo

'; + var element = el(html); + var shim = createShim(SomeComponent); + shim.shimContentElement(element); + expect(DOM.getOuterHTML(element)).toEqual('

foo

'); + }); + + it('should not transform host elements', () => { + var html = '

foo

'; + var element = el(html); + var shim = createShim(SomeComponent); + shim.shimHostElement(element); + expect(DOM.getOuterHTML(element)).toEqual('

foo

'); + }); + + it('should generate the same output for the same component', () => { + var html = '

foo

'; + var content1 = el(html); + var host1 = el(html); + var css = '.foo {color: blue;} :host{color: red;}'; + var shim1 = createShim(SomeComponent); + shim1.shimContentElement(content1); + shim1.shimHostElement(host1); + var shimCss1 = shim1.shimCssText(css); + + var content2 = el(html); + var host2 = el(html); + var shim2 = createShim(SomeComponent); + shim2.shimContentElement(content2); + shim2.shimHostElement(host2); + var shimCss2 = shim2.shimCssText(css); + + expect(DOM.getOuterHTML(content1)).toEqual(DOM.getOuterHTML(content2)); + expect(DOM.getOuterHTML(host1)).toEqual(DOM.getOuterHTML(host2)); + expect(shimCss1).toEqual(shimCss2); + }); + + it('should generate different outputs for different components', () => { + var html = '

foo

'; + var content1 = el(html); + var host1 = el(html); + var css = '.foo {color: blue;} :host{color: red;}'; + var shim1 = createShim(SomeComponent); + shim1.shimContentElement(content1); + shim1.shimHostElement(host1); + var shimCss1 = shim1.shimCssText(css); + + var content2 = el(html); + var host2 = el(html); + var shim2 = createShim(SomeComponent2); + shim2.shimContentElement(content2); + shim2.shimHostElement(host2); + var shimCss2 = shim2.shimCssText(css); + + expect(DOM.getOuterHTML(content1)).not.toEqual(DOM.getOuterHTML(content2)); + expect(DOM.getOuterHTML(host1)).not.toEqual(DOM.getOuterHTML(host2)); + expect(shimCss1).not.toEqual(shimCss2); + }); + }); + }); +} + +class SomeComponent {} +class SomeComponent2 {} diff --git a/modules/angular2/test/core/compiler/shadow_dom/shim_css_spec.js b/modules/angular2/test/core/compiler/shadow_dom/shim_css_spec.js deleted file mode 100644 index 99cf2dd6b1..0000000000 --- a/modules/angular2/test/core/compiler/shadow_dom/shim_css_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'angular2/test_lib'; -import {shimCssText} from 'angular2/src/core/compiler/shadow_dom_emulation/shim_css'; - -import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang'; - -export function main() { - describe('shim css', function() { - - function s(css: string, tag:string) { - var shim = shimCssText(css, tag); - var nlRegexp = RegExpWrapper.create('\\n'); - return StringWrapper.replaceAll(shim, nlRegexp, ''); - } - - it('should handle empty string', () => { - expect(s('', 'a')).toEqual(''); - }); - - it('should add an attribute to every rule', () => { - var css = 'one {color: red;}two {color: red;}'; - var expected = 'one[a] {color: red;}two[a] {color: red;}'; - expect(s(css, 'a')).toEqual(expected); - }); - - it('should hanlde invalid css', () => { - var css = 'one {color: red;}garbage'; - var expected = 'one[a] {color: red;}'; - expect(s(css, 'a')).toEqual(expected); - }); - - it('should add an attribute to every selector', () => { - var css = 'one, two {color: red;}'; - var expected = 'one[a], two[a] {color: red;}'; - expect(s(css, 'a')).toEqual(expected); - }); - - it('should handle media rules', () => { - var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}}'; - var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}'; - expect(s(css, 'a')).toEqual(expected); - }); - - it('should handle media rules with simple rules', () => { - var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}} div {}'; - var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}div[a] {}'; - expect(s(css, 'a')).toEqual(expected); - }); - - it('should handle complicated selectors', () => { - expect(s('one::before {}', 'a')).toEqual('one[a]::before {}'); - expect(s('one two {}', 'a')).toEqual('one[a] two[a] {}'); - expect(s('one>two {}', 'a')).toEqual('one[a]>two[a] {}'); - expect(s('one+two {}', 'a')).toEqual('one[a]+two[a] {}'); - expect(s('one~two {}', 'a')).toEqual('one[a]~two[a] {}'); - expect(s('.one.two > three {}', 'a')).toEqual('.one.two[a]>three[a] {}'); - expect(s('one[attr="value"] {}', 'a')).toEqual('one[attr="value"][a] {}'); - expect(s('one[attr=value] {}', 'a')).toEqual('one[attr=value][a] {}'); - expect(s('one[attr^="value"] {}', 'a')).toEqual('one[attr^="value"][a] {}'); - expect(s('one[attr\$="value"] {}', 'a')).toEqual('one[attr\$="value"][a] {}'); - expect(s('one[attr*="value"] {}', 'a')).toEqual('one[attr*="value"][a] {}'); - expect(s('one[attr|="value"] {}', 'a')).toEqual('one[attr|="value"][a] {}'); - expect(s('one[attr] {}', 'a')).toEqual('one[attr][a] {}'); - expect(s('[is="one"] {}', 'a')).toEqual('one[a] {}'); - }); - - it('should handle :host', () => { - expect(s(':host {}', 'a')).toEqual('a {}'); - expect(s(':host(.x,.y) {}', 'a')).toEqual('a.x, a.y {}'); - }); - - it('should support polyfill-next-selector', () => { - var css = s("polyfill-next-selector {content: 'x > y'} z {}", 'a'); - expect(css).toEqual('x[a]>y[a] {}'); - - css = s('polyfill-next-selector {content: "x > y"} z {}', 'a'); - expect(css).toEqual('x[a]>y[a] {}'); - }); - - it('should support polyfill-unscoped-next-selector', () => { - var css = s("polyfill-unscoped-next-selector {content: 'x > y'} z {}", 'a'); - expect(css).toEqual('x > y {}'); - - css = s('polyfill-unscoped-next-selector {content: "x > y"} z {}', 'a'); - expect(css).toEqual('x > y {}'); - }); - - it('should support polyfill-non-strict-next-selector', () => { - var css = s('polyfill-non-strict {} one, two {}', 'a'); - expect(css).toEqual('a one, a two {}'); - }); - - it('should handle ::shadow', () => { - var css = s('polyfill-non-strict {} x::shadow > y {}', 'a'); - expect(css).toEqual('a x > y {}'); - }); - - it('should handle /deep/', () => { - var css = s('polyfill-non-strict {} x /deep/ y {}', 'a'); - expect(css).toEqual('a x y {}'); - }); - }); -}