diff --git a/lib/core_dom/component_css_loader.dart b/lib/core_dom/component_css_loader.dart new file mode 100644 index 000000000..3f64d3926 --- /dev/null +++ b/lib/core_dom/component_css_loader.dart @@ -0,0 +1,69 @@ +part of angular.core.dom_internal; + +class ComponentCssLoader { + final Http http; + final TemplateCache templateCache; + final WebPlatformShim platformShim; + final ComponentCssRewriter componentCssRewriter; + final dom.NodeTreeSanitizer treeSanitizer; + final Map<_ComponentAssetKey, async.Future> styleElementCache; + + ComponentCssLoader(this.http, this.templateCache, this.platformShim, + this.componentCssRewriter, this.treeSanitizer, + this.styleElementCache); + + async.Future> call(String tag, List cssUrls) => + async.Future.wait(cssUrls.map((url) => _styleElement(tag, url))); + + async.Future _styleElement(String tag, String cssUrl) { + final element = styleElementCache.putIfAbsent( + new _ComponentAssetKey(tag, cssUrl), + () => _loadNewCss(tag, cssUrl)); + return element.then((e) => e.clone(true)); + } + + async.Future _loadNewCss(String tag, String cssUrl) { + return _fetch(cssUrl) + .then((css) => _shim(css, tag, cssUrl)) + .then(_buildStyleElement); + } + + async.Future _fetch(String cssUrl) { + return http.get(cssUrl, cache: templateCache) + .then((resp) => resp.responseText, onError: (e) => '/* $e */'); + } + + String _shim(String css, String tag, String cssUrl) { + final shimmed = platformShim.shimCss(css, selector: tag, cssUrl: cssUrl); + return componentCssRewriter(shimmed, selector: tag, cssUrl: cssUrl); + } + + dom.StyleElement _buildStyleElement(String css) { + var styleElement = new dom.StyleElement()..appendText(css); + treeSanitizer.sanitizeTree(styleElement); + return styleElement; + } +} + +class _ComponentAssetKey { + final String tag; + final String assetUrl; + + final String _key; + + _ComponentAssetKey(String tag, String assetUrl) + : _key = "$tag|$assetUrl", + this.tag = tag, + this.assetUrl = assetUrl; + + @override + String toString() => _key; + + @override + int get hashCode => _key.hashCode; + + bool operator ==(key) => + key is _ComponentAssetKey + && tag == key.tag + && assetUrl == key.assetUrl; +} \ No newline at end of file diff --git a/lib/core_dom/css_shim.dart b/lib/core_dom/css_shim.dart new file mode 100644 index 000000000..49f83e9f3 --- /dev/null +++ b/lib/core_dom/css_shim.dart @@ -0,0 +1,367 @@ +library css_shim; + +import 'package:angular/core/parser/characters.dart'; + +String shimCssText(String css, String tag) => + new _CssShim(tag).shimCssText(css); + + +/** + * This is a shim for ShadowDOM css styling. It adds an attribute selector suffix + * to each simple selector. + * + * So: + * + * one, two {color: red;} + * + * Becomes: + * + * one[tag], two[tag] {color: red;} + * + * It can handle the following selectors: + * * `one::before` + * * `one two` + * * `one>two` + * * `one+two` + * * `one~two` + * * `.one.two` + * * `one[attr="value"]` + * * `one[attr^="value"]` + * * `one[attr$="value"]` + * * `one[attr*="value"]` + * * `one[attr|="value"]` + * * `one[attr]` + * * `[is=one]` + * + * It can handle :host: + * * `:host` + * * `:host(.x)` + * + * When the shim is not powerful enough, you can fall back on the polyfill-next-selector + * directive. + * + * polyfill-next-selector {content: 'x > y'} + * z {} + * + * Becomes: + * + * x[tag] > y[tag] + * + * See http://www.polymer-project.org/docs/polymer/styling.html#at-polyfill + * + * This implementation is a simplified version of the shim provided by platform.js: + * https://github.com/Polymer/platform-dev/blob/master/src/ShadowCSS.js + */ +class _CssShim { + static final List SELECTOR_SPLITS = const [' ', '>', '+', '~']; + static final RegExp POLYFILL_NEXT_SELECTOR_DIRECTIVE = new RegExp( + r"polyfill-next-selector" + r"[^}]*" + r"content\:[\s]*" + r"'([^']*)'" + r"[^}]*}" + r"([^{]*)", + caseSensitive: false, + multiLine: true + ); + static final int NEXT_SELECTOR_CONTENT = 1; + + static final String HOST_TOKEN = '-host-element'; + static final RegExp COLON_SELECTORS = new RegExp(r'(' + HOST_TOKEN + r')(\(.*\)){0,1}(.*)', + caseSensitive: false); + static final RegExp SIMPLE_SELECTORS = new RegExp(r'([^:]*)(:*)(.*)', caseSensitive: false); + static final RegExp IS_SELECTORS = new RegExp(r'\[is="([^\]]*)"\]', caseSensitive: false); + + // See https://github.com/Polymer/platform-dev/blob/master/src/ShadowCSS.js#L561 + static final String PAREN_SUFFIX = r')(?:\((' + r'(?:\([^)(]*\)|[^)(]*)+?' + r')\))?([^,{]*)'; + static final RegExp COLON_HOST = new RegExp('($HOST_TOKEN$PAREN_SUFFIX', + caseSensitive: false, multiLine: true); + + final String tag; + final String attr; + + _CssShim(String tag) + : tag = tag, + attr = "[$tag]"; + + String shimCssText(String css) { + final preprocessed = convertColonHost(applyPolyfillNextSelectorDirective(css)); + final rules = cssToRules(preprocessed); + return scopeRules(rules); + } + + String applyPolyfillNextSelectorDirective(String css) => + css.replaceAllMapped(POLYFILL_NEXT_SELECTOR_DIRECTIVE, (m) => m[NEXT_SELECTOR_CONTENT]); + + String convertColonHost(String css) { + css = css.replaceAll(":host", HOST_TOKEN); + + String partReplacer(host, part, suffix) => + "$host${part.replaceAll(HOST_TOKEN, '')}$suffix"; + + return css.replaceAllMapped(COLON_HOST, (m) { + final base = HOST_TOKEN; + final inParens = m.group(2); + final rest = m.group(3); + + if (inParens != null && inParens.isNotEmpty) { + return inParens.split(',') + .map((p) => p.trim()) + .where((_) => _.isNotEmpty) + .map((p) => partReplacer(base, p, rest)) + .join(","); + } else { + return "$base$rest"; + } + }); + } + + List<_Rule> cssToRules(String css) => + new _Parser(css).parse(); + + String scopeRules(List<_Rule> rules) => + rules.map(scopeRule).join("\n"); + + String scopeRule(_Rule rule) { + if (rule.hasNestedRules) { + final selector = rule.selectorText; + final rules = scopeRules(rule.rules); + return '$selector {\n$rules\n}'; + } else { + final scopedSelector = scopeSelector(rule.selectorText); + final scopedBody = cssText(rule); + return "$scopedSelector $scopedBody"; + } + } + + String scopeSelector(String selector) { + final parts = selector.split(","); + final scopedParts = parts.fold([], (res, p) { + res.add(scopeSimpleSelector(p.trim())); + return res; + }); + return scopedParts.join(", "); + } + + String scopeSimpleSelector(String selector) { + if (selector.contains(HOST_TOKEN)) { + return replaceColonSelectors(selector); + } else { + return insertTag(selector); + } + } + + String cssText(_Rule rule) => rule.body; + + String replaceColonSelectors(String css) { + return css.replaceAllMapped(COLON_SELECTORS, (m) { + final selectorInParens = m[2] == null ? "" : m[2].substring(1, m[2].length - 1); + final rest = m[3]; + return "$tag$selectorInParens$rest"; + }); + } + + String insertTag(String selector) { + selector = handleIsSelector(selector); + + SELECTOR_SPLITS.forEach((split) { + final parts = selector.split(split).map((p) => p.trim()); + selector = parts.map(insertAttrSuffixIntoSelectorPart).join(split); + }); + + return selector; + } + + String insertAttrSuffixIntoSelectorPart(String p) { + final shouldInsert = p.isNotEmpty && !SELECTOR_SPLITS.contains(p) && !p.contains(attr); + return shouldInsert ? insertAttr(p) : p; + } + + String insertAttr(String selector) { + return selector.replaceAllMapped(SIMPLE_SELECTORS, (m) { + final basePart = m[1]; + final colonPart = m[2]; + final rest = m[3]; + return m[0].isNotEmpty ? "$basePart$attr$colonPart$rest" : ""; + }); + } + + String handleIsSelector(String selector) => + selector.replaceAllMapped(IS_SELECTORS, (m) => m[1]); +} + + + +class _Token { + static final _Token EOF = new _Token(null); + final String string; + final String type; + _Token(this.string, [this.type]); + + String toString() => "TOKEN[$string, $type]"; +} + +class _Lexer { + int peek = 0; + int index = -1; + final String input; + final int length; + + _Lexer(String input) + : input = input, + length = input.length { + advance(); + } + + List<_Token> parse() { + final res = []; + var t = scanToken(); + while (t != _Token.EOF) { + res.add(t); + t = scanToken(); + } + return res; + } + + _Token scanToken() { + skipWhitespace(); + + if (peek == $EOF) return _Token.EOF; + if (isBodyEnd(peek)) { + advance(); + return new _Token("}", "rparen"); + } + if (isMedia(peek)) return scanMedia(); + if (isSelector(peek)) return scanSelector(); + if (isBodyStart(peek)) return scanBody(); + + return _Token.EOF; + } + + bool isSelector(int v) => !isBodyStart(v) && v != $EOF; + bool isBodyStart(int v) => v == $LBRACE; + bool isBodyEnd(int v) => v == $RBRACE; + bool isMedia(int v) => v == 64; //@ = 64 + + void skipWhitespace() { + while (isWhitespace(peek)) { + if (++index >= length) { + peek = $EOF; + return null; + } else { + peek = input.codeUnitAt(index); + } + } + } + + _Token scanSelector() { + int start = index; + advance(); + while (isSelector(peek)) advance(); + String string = input.substring(start, index); + return new _Token(string, "selector"); + } + + _Token scanBody() { + int start = index; + advance(); + while (!isBodyEnd(peek)) advance(); + advance(); + String string = input.substring(start, index); + return new _Token(string, "body"); + } + + _Token scanMedia() { + int start = index; + advance(); + + while (!isBodyStart(peek)) advance(); + String string = input.substring(start, index); + + advance(); //skip { + + return new _Token(string, "media"); + } + + void advance() { + peek = ++index >= length ? $EOF : input.codeUnitAt(index); + } +} + +class _Rule { + final String selectorText; + final String body; + final List<_Rule> rules; + + _Rule(this.selectorText, {this.body, this.rules}); + + bool get hasNestedRules => rules != null; + + String toString() => "Rule[$selectorText $body]"; +} + +class _Parser { + List<_Token> tokens; + int currentIndex; + + _Parser(String input) { + tokens = new _Lexer(input).parse(); + currentIndex = -1; + } + + List<_Rule> parse() { + final res = []; + var rule; + while ((rule = parseRule()) != null) { + res.add(rule); + } + return res; + } + + _Rule parseRule() { + try { + if (next.type == "media") { + return parseMedia(); + } else { + return parseCssRule(); + } + } catch (e) { + return null; + } + } + + _Rule parseMedia() { + advance("media"); + final media = current.string; + + final rules = []; + while (next.type != "rparen") { + rules.add(parseCssRule()); + } + advance("rparen"); + + return new _Rule(media.trim(), rules: rules); + } + + _Rule parseCssRule() { + advance("selector"); + final selector = current.string; + + advance("body"); + final body = current.string; + + return new _Rule(selector, body: body); + } + + void advance(String expectedType) { + currentIndex += 1; + if (current.type != expectedType) { + throw "Unexpected token ${current.type}. Expected $expectedType"; + } + } + + _Token get current => tokens[currentIndex]; + _Token get next => tokens[currentIndex + 1]; +} \ No newline at end of file diff --git a/lib/core_dom/directive_injector.dart b/lib/core_dom/directive_injector.dart index 7552e4642..9a0d5d19f 100644 --- a/lib/core_dom/directive_injector.dart +++ b/lib/core_dom/directive_injector.dart @@ -14,7 +14,8 @@ import 'package:angular/core/module.dart' show Scope, RootScope; import 'package:angular/core/annotation.dart' show Visibility, DirectiveBinder; import 'package:angular/core_dom/module_internal.dart' show Animate, View, ViewFactory, BoundViewFactory, ViewPort, NodeAttrs, ElementProbe, - NgElement, DestinationLightDom, SourceLightDom, LightDom, TemplateLoader, ShadowRootEventHandler, EventHandler; + NgElement, DestinationLightDom, SourceLightDom, LightDom, TemplateLoader, ShadowRootEventHandler, + EventHandler, ShadowBoundary, DefaultShadowBoundary; var _TAG_GET = new UserTag('DirectiveInjector.get()'); var _TAG_INSTANTIATE = new UserTag('DirectiveInjector.instantiate()'); @@ -58,8 +59,9 @@ const int SHADOW_ROOT_KEY_ID = 15; const int DESTINATION_LIGHT_DOM_KEY_ID = 16; const int SOURCE_LIGHT_DOM_KEY_ID = 17; const int EVENT_HANDLER_KEY_ID = 18; -const int COMPONENT_DIRECTIVE_INJECTOR_KEY_ID = 19; -const int KEEP_ME_LAST = 20; +const int SHADOW_BOUNDARY_KEY_ID = 19; +const int COMPONENT_DIRECTIVE_INJECTOR_KEY_ID = 20; +const int KEEP_ME_LAST = 21; EventHandler eventHandler(DirectiveInjector di) => di._eventHandler; @@ -68,25 +70,26 @@ class DirectiveInjector implements DirectiveBinder { static initUID() { if (_isInit) return; _isInit = true; - INJECTOR_KEY.uid = INJECTOR_KEY_ID; - DIRECTIVE_INJECTOR_KEY.uid = DIRECTIVE_INJECTOR_KEY_ID; - NODE_KEY.uid = NODE_KEY_ID; - ELEMENT_KEY.uid = ELEMENT_KEY_ID; - NODE_ATTRS_KEY.uid = NODE_ATTRS_KEY_ID; - SCOPE_KEY.uid = SCOPE_KEY_ID; - VIEW_KEY.uid = VIEW_KEY_ID; - VIEW_PORT_KEY.uid = VIEW_PORT_KEY_ID; - VIEW_FACTORY_KEY.uid = VIEW_FACTORY_KEY_ID; - NG_ELEMENT_KEY.uid = NG_ELEMENT_KEY_ID; - BOUND_VIEW_FACTORY_KEY.uid = BOUND_VIEW_FACTORY_KEY_ID; - ELEMENT_PROBE_KEY.uid = ELEMENT_PROBE_KEY_ID; - TEMPLATE_LOADER_KEY.uid = TEMPLATE_LOADER_KEY_ID; - SHADOW_ROOT_KEY.uid = SHADOW_ROOT_KEY_ID; - DESTINATION_LIGHT_DOM_KEY.uid = DESTINATION_LIGHT_DOM_KEY_ID; - SOURCE_LIGHT_DOM_KEY.uid = SOURCE_LIGHT_DOM_KEY_ID; - EVENT_HANDLER_KEY.uid = EVENT_HANDLER_KEY_ID; - ANIMATE_KEY.uid = ANIMATE_KEY_ID; + INJECTOR_KEY.uid = INJECTOR_KEY_ID; + DIRECTIVE_INJECTOR_KEY.uid = DIRECTIVE_INJECTOR_KEY_ID; + NODE_KEY.uid = NODE_KEY_ID; + ELEMENT_KEY.uid = ELEMENT_KEY_ID; + NODE_ATTRS_KEY.uid = NODE_ATTRS_KEY_ID; + SCOPE_KEY.uid = SCOPE_KEY_ID; + VIEW_KEY.uid = VIEW_KEY_ID; + VIEW_PORT_KEY.uid = VIEW_PORT_KEY_ID; + VIEW_FACTORY_KEY.uid = VIEW_FACTORY_KEY_ID; + NG_ELEMENT_KEY.uid = NG_ELEMENT_KEY_ID; + BOUND_VIEW_FACTORY_KEY.uid = BOUND_VIEW_FACTORY_KEY_ID; + ELEMENT_PROBE_KEY.uid = ELEMENT_PROBE_KEY_ID; + TEMPLATE_LOADER_KEY.uid = TEMPLATE_LOADER_KEY_ID; + SHADOW_ROOT_KEY.uid = SHADOW_ROOT_KEY_ID; + DESTINATION_LIGHT_DOM_KEY.uid = DESTINATION_LIGHT_DOM_KEY_ID; + SOURCE_LIGHT_DOM_KEY.uid = SOURCE_LIGHT_DOM_KEY_ID; + EVENT_HANDLER_KEY.uid = EVENT_HANDLER_KEY_ID; + SHADOW_BOUNDARY_KEY.uid = SHADOW_BOUNDARY_KEY_ID; COMPONENT_DIRECTIVE_INJECTOR_KEY.uid = COMPONENT_DIRECTIVE_INJECTOR_KEY_ID; + ANIMATE_KEY.uid = ANIMATE_KEY_ID; for(var i = 1; i < KEEP_ME_LAST; i++) { if (_KEYS[i].uid != i) throw 'MISSORDERED KEYS ARRAY: ${_KEYS} at $i'; } @@ -111,6 +114,7 @@ class DirectiveInjector implements DirectiveBinder { , DESTINATION_LIGHT_DOM_KEY , SOURCE_LIGHT_DOM_KEY , EVENT_HANDLER_KEY + , SHADOW_BOUNDARY_KEY , COMPONENT_DIRECTIVE_INJECTOR_KEY , KEEP_ME_LAST ]; @@ -121,6 +125,7 @@ class DirectiveInjector implements DirectiveBinder { final NodeAttrs _nodeAttrs; final Animate _animate; final EventHandler _eventHandler; + final ShadowBoundary _shadowBoundary; LightDom lightDom; Scope scope; //TODO(misko): this should be final after we get rid of controller final View _view; @@ -164,20 +169,18 @@ class DirectiveInjector implements DirectiveBinder { static Binding _tempBinding = new Binding(); DirectiveInjector(DirectiveInjector parent, appInjector, this._node, this._nodeAttrs, - this._eventHandler, this.scope, this._animate, [View view]) + this._eventHandler, this.scope, this._animate, [View view, ShadowBoundary boundary]) : _parent = parent, - _appInjector = appInjector, - _view = view == null && parent != null ? parent._view : view, - _constructionDepth = NO_CONSTRUCTION; - - DirectiveInjector._default(this._parent, this._appInjector) - : _node = null, - _nodeAttrs = null, - _eventHandler = null, - scope = null, - _view = null, - _animate = null, - _constructionDepth = NO_CONSTRUCTION; + _appInjector = appInjector, + _view = view == null && parent != null ? parent._view : view, + _constructionDepth = NO_CONSTRUCTION, + _shadowBoundary = _getShadowBoundary(boundary, parent); + + static _getShadowBoundary(ShadowBoundary boundary, DirectiveInjector parent) { + if (boundary != null) return boundary; + if (parent != null) return parent._shadowBoundary; + return new DefaultShadowBoundary(); + } void bind(key, {dynamic toValue: DEFAULT_VALUE, Function toFactory: DEFAULT_VALUE, @@ -314,6 +317,7 @@ class DirectiveInjector implements DirectiveBinder { case ELEMENT_PROBE_KEY_ID: return elementProbe; case NG_ELEMENT_KEY_ID: return ngElement; case EVENT_HANDLER_KEY_ID: return _eventHandler; + case SHADOW_BOUNDARY_KEY_ID: return _shadowBoundary; case DESTINATION_LIGHT_DOM_KEY_ID: return _destLightDom; case SOURCE_LIGHT_DOM_KEY_ID: return _sourceLightDom; case VIEW_KEY_ID: return _view; @@ -413,8 +417,10 @@ class TemplateDirectiveInjector extends DirectiveInjector { TemplateDirectiveInjector(DirectiveInjector parent, Injector appInjector, Node node, NodeAttrs nodeAttrs, EventHandler eventHandler, - Scope scope, Animate animate, this._viewFactory, [View view]) - : super(parent, appInjector, node, nodeAttrs, eventHandler, scope, animate, view); + Scope scope, Animate animate, this._viewFactory, + [View view, ShadowBoundary shadowBoundary]) + : super(parent, appInjector, node, nodeAttrs, eventHandler, scope, animate, + view, shadowBoundary); Object _getById(int keyId) { @@ -443,9 +449,10 @@ class ComponentDirectiveInjector extends DirectiveInjector { ComponentDirectiveInjector(DirectiveInjector parent, Injector appInjector, EventHandler eventHandler, Scope scope, - this._templateLoader, this._shadowRoot, LightDom lightDom, [View view]) + this._templateLoader, this._shadowRoot, LightDom lightDom, + [View view, ShadowBoundary shadowBoundary]) : super(parent, appInjector, parent._node, parent._nodeAttrs, eventHandler, scope, - parent._animate, view) { + parent._animate, view, shadowBoundary) { // A single component creates a ComponentDirectiveInjector and its DirectiveInjector parent, // so parent should never be null. assert(parent != null); diff --git a/lib/core_dom/module_internal.dart b/lib/core_dom/module_internal.dart index f0cd7e385..128f98cfc 100644 --- a/lib/core_dom/module_internal.dart +++ b/lib/core_dom/module_internal.dart @@ -8,7 +8,9 @@ import 'dart:js' as js; import 'package:di/di.dart'; import 'package:di/annotations.dart'; import 'package:perf_api/perf_api.dart'; +import 'package:logging/logging.dart'; +import 'package:angular/utils.dart'; import 'package:angular/cache/module.dart'; import 'package:angular/core/annotation.dart'; @@ -25,6 +27,8 @@ import 'package:angular/core/registry.dart'; import 'package:angular/ng_tracing.dart'; import 'package:angular/directive/module.dart' show NgBaseCss; +import 'package:angular/core_dom/css_shim.dart' as cssShim; + import 'dart:collection'; part 'animation.dart'; @@ -37,6 +41,7 @@ part 'directive_map.dart'; part 'element_binder.dart'; part 'element_binder_builder.dart'; part 'event_handler.dart'; +part 'shadow_boundary.dart'; part 'http.dart'; part 'mustache.dart'; part 'ng_element.dart'; @@ -46,12 +51,13 @@ part 'shadow_dom_component_factory.dart'; part 'emulated_shadow_root.dart'; part 'template_cache.dart'; part 'transcluding_component_factory.dart'; +part 'component_css_loader.dart'; part 'light_dom.dart'; part 'content_tag.dart'; part 'tree_sanitizer.dart'; part 'view.dart'; part 'view_factory.dart'; -part 'web_platform.dart'; +part 'web_platform_shim.dart'; class CoreDomModule extends Module { CoreDomModule() { @@ -78,7 +84,8 @@ class CoreDomModule extends Module { bind(DestinationLightDom, toValue: null); bind(SourceLightDom, toValue: null); bind(ComponentCssRewriter); - bind(WebPlatform); + bind(PlatformJsBasedShim); + bind(DefaultPlatformShim); bind(Http); bind(UrlRewriter); @@ -99,6 +106,5 @@ class CoreDomModule extends Module { bind(EventHandler); // TODO(rkirov): remove this once clients have stopped relying on it. bind(DirectiveInjector, toValue: null); - } } diff --git a/lib/core_dom/shadow_boundary.dart b/lib/core_dom/shadow_boundary.dart new file mode 100644 index 000000000..048fe31f3 --- /dev/null +++ b/lib/core_dom/shadow_boundary.dart @@ -0,0 +1,33 @@ +part of angular.core.dom_internal; + +/** +* The root of the application has a [ShadowBoundary] attached as does every [Component]. +* +* [ShadowBoundary] is responsible for inserting style elements. +*/ +@Injectable() +abstract class ShadowBoundary { + void insertStyleElements(List elements); +} + +@Injectable() +class DefaultShadowBoundary implements ShadowBoundary { + void insertStyleElements(List elements) { + dom.document.head.nodes.addAll(elements); + } +} + +@Injectable() +class ShadowRootBoundary implements ShadowBoundary { + final dom.ShadowRoot shadowRoot; + dom.StyleElement _lastStyleElement; + + ShadowRootBoundary(this.shadowRoot); + + void insertStyleElements(List elements) { + if (elements.isEmpty) return; + final n = _lastStyleElement == null ? null : _lastStyleElement.nextNode; + shadowRoot.insertAllBefore(elements, n); + _lastStyleElement = elements.last; + } +} diff --git a/lib/core_dom/shadow_dom_component_factory.dart b/lib/core_dom/shadow_dom_component_factory.dart index f34e27bd0..457673898 100644 --- a/lib/core_dom/shadow_dom_component_factory.dart +++ b/lib/core_dom/shadow_dom_component_factory.dart @@ -36,20 +36,20 @@ abstract class BoundComponentFactory { @Injectable() class ShadowDomComponentFactory implements ComponentFactory { final ViewCache viewCache; - final Http http; - final TemplateCache templateCache; - final WebPlatform platform; - final ComponentCssRewriter componentCssRewriter; - final dom.NodeTreeSanitizer treeSanitizer; + final PlatformJsBasedShim platformShim; final Expando expando; final CompilerConfig config; + ComponentCssLoader cssLoader; - final Map<_ComponentAssetKey, async.Future> styleElementCache = {}; - - ShadowDomComponentFactory(this.viewCache, this.http, this.templateCache, this.platform, - this.componentCssRewriter, this.treeSanitizer, this.expando, - this.config, CacheRegister cacheRegister) { + ShadowDomComponentFactory(this.viewCache, this.platformShim, this.expando, this.config, + Http http, TemplateCache templateCache, ComponentCssRewriter componentCssRewriter, + dom.NodeTreeSanitizer treeSanitizer, CacheRegister cacheRegister) { + final styleElementCache = new HashMap(); cacheRegister.registerCache("ShadowDomComponentFactoryStyles", styleElementCache); + + cssLoader = new ComponentCssLoader(http, templateCache, platformShim, + componentCssRewriter, treeSanitizer, styleElementCache); + } bind(DirectiveRef ref, directives, injector) => @@ -60,7 +60,6 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory { final ShadowDomComponentFactory _componentFactory; final DirectiveRef _ref; - final DirectiveMap _directives; final Injector _injector; Component get _component => _ref.annotation as Component; @@ -69,62 +68,20 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory { async.Future> _styleElementsFuture; async.Future _viewFuture; - BoundShadowDomComponentFactory(this._componentFactory, this._ref, this._directives, this._injector) { - _tag = _component.selector.toLowerCase(); - _styleElementsFuture = async.Future.wait(_component.cssUrls.map(_styleFuture)); + BoundShadowDomComponentFactory(this._componentFactory, this._ref, + DirectiveMap directives, this._injector) { + _tag = _ref.annotation.selector.toLowerCase(); + _styleElementsFuture = _componentFactory.cssLoader(_tag, _component.cssUrls); - _viewFuture = BoundComponentFactory._viewFuture( - _component, - new PlatformViewCache(_componentFactory.viewCache, _tag, _componentFactory.platform), - _directives); - } - - async.Future _styleFuture(cssUrl) { - Http http = _componentFactory.http; - TemplateCache templateCache = _componentFactory.templateCache; - WebPlatform platform = _componentFactory.platform; - ComponentCssRewriter componentCssRewriter = _componentFactory.componentCssRewriter; - dom.NodeTreeSanitizer treeSanitizer = _componentFactory.treeSanitizer; - - return _componentFactory.styleElementCache.putIfAbsent( - new _ComponentAssetKey(_tag, cssUrl), () => - http.get(cssUrl, cache: templateCache) - .then((resp) => resp.responseText, - onError: (e) => '/*\n$e\n*/\n') - .then((String css) { - - // Shim CSS if required - if (platform.cssShimRequired) { - css = platform.shimCss(css, selector: _tag, cssUrl: cssUrl); - } - - // If a css rewriter is installed, run the css through a rewriter - var styleElement = new dom.StyleElement() - ..appendText(componentCssRewriter(css, selector: _tag, - cssUrl: cssUrl)); - - // ensure there are no invalid tags or modifications - treeSanitizer.sanitizeTree(styleElement); - - // If the css shim is required, it means that scoping does not - // work, and adding the style to the head of the document is - // preferrable. - if (platform.cssShimRequired) { - dom.document.head.append(styleElement); - return null; - } - - return styleElement; - }) - ); + final viewCache = new ShimmingViewCache(_componentFactory.viewCache, + _tag, _componentFactory.platformShim); + _viewFuture = BoundComponentFactory._viewFuture(_component, viewCache, directives); } List get callArgs => _CALL_ARGS; - static final _CALL_ARGS = [DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, VIEW_KEY, NG_BASE_CSS_KEY, - EVENT_HANDLER_KEY]; + static final _CALL_ARGS = [DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, VIEW_KEY, NG_BASE_CSS_KEY]; Function call(dom.Element element) { - return (DirectiveInjector injector, Scope scope, View view, NgBaseCss baseCss, - EventHandler _) { + return (DirectiveInjector injector, Scope scope, View view, NgBaseCss baseCss) { var s = traceEnter(View_createComponent); try { var shadowDom = element.createShadowRoot() @@ -132,40 +89,40 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory { ..resetStyleInheritance = _component.resetStyleInheritance; var shadowScope = scope.createChild(new HashMap()); // Isolate + ComponentDirectiveInjector shadowInjector; - async.Future> cssFuture; - if (_component.useNgBaseCss == true) { - cssFuture = async.Future.wait([async.Future.wait(baseCss.urls.map(_styleFuture)), _styleElementsFuture]).then((twoLists) { - assert(twoLists.length == 2);return [] - ..addAll(twoLists[0]) - ..addAll(twoLists[1]); - }); - } else { - cssFuture = _styleElementsFuture; - } + final baseUrls = (_component.useNgBaseCss) ? baseCss.urls : []; + final baseUrlsFuture = _componentFactory.cssLoader(_tag, baseUrls); + final cssFuture = mergeFutures(baseUrlsFuture, _styleElementsFuture); - ComponentDirectiveInjector shadowInjector; + void insertStyleElements(els) { + final shadowBoundary = shadowInjector.get(ShadowBoundary); + shadowBoundary.insertStyleElements(els); + } - TemplateLoader templateLoader = new TemplateLoader(cssFuture.then((Iterable cssList) { - cssList.where((styleElement) => styleElement != null).forEach((styleElement) { - shadowDom.append(styleElement.clone(true)); + async.Future initShadowDom(_) { + if (_viewFuture == null) return new async.Future.value(shadowDom); + return _viewFuture.then((ViewFactory viewFactory) { + if (shadowScope.isAttached) { + shadowDom.nodes.addAll( + viewFactory.call(shadowInjector.scope, shadowInjector).nodes); + } + return shadowDom; }); - if (_viewFuture != null) { - return _viewFuture.then((ViewFactory viewFactory) { - if (shadowScope.isAttached) { - shadowDom.nodes.addAll(viewFactory.call(shadowInjector.scope, shadowInjector).nodes); - } - return shadowDom; - }); - } - return shadowDom; - })); + } + + TemplateLoader templateLoader = new TemplateLoader( + cssFuture.then(insertStyleElements).then(initShadowDom)); var probe; var eventHandler = new ShadowRootEventHandler( shadowDom, injector.getByKey(EXPANDO_KEY), injector.getByKey(EXCEPTION_HANDLER_KEY)); + var shadowBoundary = new ShadowRootBoundary(shadowDom); + shadowInjector = new ComponentDirectiveInjector(injector, _injector, eventHandler, shadowScope, - templateLoader, shadowDom, null, view); + templateLoader, shadowDom, null, view, shadowBoundary); + + shadowInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); if (_componentFactory.config.elementProbeEnabled) { @@ -186,29 +143,6 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory { } } -class _ComponentAssetKey { - final String tag; - final String assetUrl; - - final String _key; - - _ComponentAssetKey(String tag, String assetUrl) - : _key = "$tag|$assetUrl", - this.tag = tag, - this.assetUrl = assetUrl; - - @override - String toString() => _key; - - @override - int get hashCode => _key.hashCode; - - bool operator ==(key) => - key is _ComponentAssetKey - && tag == key.tag - && assetUrl == key.assetUrl; -} - @Injectable() class ComponentCssRewriter { String call(String css, { String selector, String cssUrl} ) { diff --git a/lib/core_dom/static_keys.dart b/lib/core_dom/static_keys.dart index 8c770c9f7..8eacb1828 100644 --- a/lib/core_dom/static_keys.dart +++ b/lib/core_dom/static_keys.dart @@ -20,6 +20,7 @@ final Key DIRECTIVE_MAP_KEY = new Key(DirectiveMap); final Key ELEMENT_KEY = new Key(dom.Element); final Key ELEMENT_PROBE_KEY = new Key(ElementProbe); final Key EVENT_HANDLER_KEY = new Key(EventHandler); +final Key SHADOW_BOUNDARY_KEY = new Key(ShadowBoundary); final Key HTTP_KEY = new Key(Http); final Key NG_ELEMENT_KEY = new Key(NgElement); final Key NODE_ATTRS_KEY = new Key(NodeAttrs); @@ -35,6 +36,8 @@ final Key VIEW_CACHE_KEY = new Key(ViewCache); final Key VIEW_FACTORY_KEY = new Key(ViewFactory); final Key VIEW_KEY = new Key(View); final Key VIEW_PORT_KEY = new Key(ViewPort); -final Key WEB_PLATFORM_KEY = new Key(WebPlatform); +final Key WEB_PLATFORM_SHIM_KEY = new Key(WebPlatformShim); +final Key DEFAULT_PLATFORM_SHIM_KEY = new Key(DefaultPlatformShim); +final Key PLATFORM_JS_BASED_SHIM_KEY = new Key(PlatformJsBasedShim); final Key WINDOW_KEY = new Key(dom.Window); final Key EXPANDO_KEY = new Key(Expando); diff --git a/lib/core_dom/transcluding_component_factory.dart b/lib/core_dom/transcluding_component_factory.dart index 0c5c0b1aa..c4a05973b 100644 --- a/lib/core_dom/transcluding_component_factory.dart +++ b/lib/core_dom/transcluding_component_factory.dart @@ -6,8 +6,18 @@ class TranscludingComponentFactory implements ComponentFactory { final Expando expando; final ViewCache viewCache; final CompilerConfig config; + final DefaultPlatformShim platformShim; + ComponentCssLoader cssLoader; - TranscludingComponentFactory(this.expando, this.viewCache, this.config); + TranscludingComponentFactory(this.expando, this.viewCache, this.config, this.platformShim, + Http http, TemplateCache templateCache, ComponentCssRewriter componentCssRewriter, + dom.NodeTreeSanitizer treeSanitizer, CacheRegister cacheRegister) { + final styleElementCache = new HashMap(); + cacheRegister.registerCache("TranscludingComponentFactoryStyles", styleElementCache); + + cssLoader = new ComponentCssLoader(http, templateCache, platformShim, + componentCssRewriter, treeSanitizer, styleElementCache); + } bind(DirectiveRef ref, directives, injector) => new BoundTranscludingComponentFactory(this, ref, directives, injector); @@ -19,29 +29,31 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory { final DirectiveMap _directives; final Injector _injector; + String _tag; + async.Future> _styleElementsFuture; + Component get _component => _ref.annotation as Component; async.Future _viewFuture; BoundTranscludingComponentFactory(this._f, this._ref, this._directives, this._injector) { - _viewFuture = BoundComponentFactory._viewFuture( - _component, - _f.viewCache, - _directives); + _tag = _ref.annotation.selector.toLowerCase(); + _styleElementsFuture = _f.cssLoader(_tag, _component.cssUrls); + + final viewCache = new ShimmingViewCache(_f.viewCache, _tag, _f.platformShim); + _viewFuture = BoundComponentFactory._viewFuture(_component, viewCache, _directives); } List get callArgs => _CALL_ARGS; static var _CALL_ARGS = [ DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, VIEW_KEY, VIEW_CACHE_KEY, HTTP_KEY, TEMPLATE_CACHE_KEY, - DIRECTIVE_MAP_KEY, NG_BASE_CSS_KEY, EVENT_HANDLER_KEY]; + DIRECTIVE_MAP_KEY, NG_BASE_CSS_KEY, EVENT_HANDLER_KEY, + SHADOW_BOUNDARY_KEY]; Function call(dom.Node node) { - // CSS is not supported. - assert(_component.cssUrls == null || - _component.cssUrls.isEmpty); - var element = node as dom.Element; return (DirectiveInjector injector, Scope scope, View view, ViewCache viewCache, Http http, TemplateCache templateCache, - DirectiveMap directives, NgBaseCss baseCss, EventHandler eventHandler) { + DirectiveMap directives, NgBaseCss baseCss, EventHandler eventHandler, + ShadowBoundary shadowBoundary) { DirectiveInjector childInjector; var childInjectorCompleter; // Used if the ViewFuture is available before the childInjector. @@ -49,28 +61,33 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory { var component = _component; var lightDom = new LightDom(element, scope); - // Append the component's template as children - var elementFuture; + final baseUrls = (_component.useNgBaseCss) ? baseCss.urls : []; + final baseUrlsFuture = _f.cssLoader(_tag, baseUrls); + final cssFuture = mergeFutures(baseUrlsFuture, _styleElementsFuture); - if (_viewFuture != null) { - elementFuture = _viewFuture.then((ViewFactory viewFactory) { - lightDom.pullNodes(); + initShadowDom(_) { + if (_viewFuture != null) { + return _viewFuture.then((ViewFactory viewFactory) { + lightDom.pullNodes(); - if (childInjector != null) { - lightDom.shadowDomView = viewFactory.call(childInjector.scope, childInjector); - return element; - } else { - childInjectorCompleter = new async.Completer(); - return childInjectorCompleter.future.then((childInjector) { + if (childInjector != null) { lightDom.shadowDomView = viewFactory.call(childInjector.scope, childInjector); return element; - }); - } - }); - } else { - elementFuture = new async.Future.microtask(() => lightDom.pullNodes()); + } else { + childInjectorCompleter = new async.Completer(); + return childInjectorCompleter.future.then((childInjector) { + lightDom.shadowDomView = viewFactory.call(childInjector.scope, childInjector); + return element; + }); + } + }); + } else { + return new async.Future.microtask(() => lightDom.pullNodes()); + } } - TemplateLoader templateLoader = new TemplateLoader(elementFuture); + + TemplateLoader templateLoader = new TemplateLoader( + cssFuture.then(shadowBoundary.insertStyleElements).then(initShadowDom)); Scope shadowScope = scope.createChild(new HashMap()); diff --git a/lib/core_dom/view_factory.dart b/lib/core_dom/view_factory.dart index 4c9952bcc..fdcaba243 100644 --- a/lib/core_dom/view_factory.dart +++ b/lib/core_dom/view_factory.dart @@ -47,8 +47,6 @@ class ViewFactory implements Function { BoundViewFactory bind(DirectiveInjector directiveInjector) => new BoundViewFactory(this, directiveInjector); - static Key _EVENT_HANDLER_KEY = new Key(EventHandler); - View call(Scope scope, DirectiveInjector directiveInjector, [List nodes /* TODO: document fragment */]) { var s = traceEnter1(View_create, _debugHtml); diff --git a/lib/core_dom/web_platform_shim.dart b/lib/core_dom/web_platform_shim.dart new file mode 100644 index 000000000..052a3a876 --- /dev/null +++ b/lib/core_dom/web_platform_shim.dart @@ -0,0 +1,137 @@ +part of angular.core.dom_internal; + +final Logger _log = new Logger('WebPlatformShim'); + +/** + * Shims for interacting with experimental platform feature that are required + * for the correct behavior of angular, but are not supported on all browsers + * without polyfills. + */ +abstract class WebPlatformShim { + String shimCss(String css, { String selector, String cssUrl }); + + void shimShadowDom(dom.Element root, String selector); + + bool get shimRequired; +} + +/** + * [PlatformJsBasedShim] is an implementation of WebPlatformShim that delegates + * css shimming to platform.js. It also uses platform.js to detect if shimming is required. + * + * See http://www.polymer-project.org/docs/polymer/styling.html + */ +@Injectable() +class PlatformJsBasedShim implements WebPlatformShim { + js.JsObject _shadowCss; + + bool get shimRequired => _shadowCss != null; + + PlatformJsBasedShim() { + var _platformJs = js.context['Platform']; + if (_platformJs != null) { + _shadowCss = _platformJs['ShadowCSS']; + if (_shadowCss != null) { + _shadowCss['strictStyling'] = true; + } + } + } + + String shimCss(String css, { String selector, String cssUrl }) { + if (! shimRequired) return css; + + var shimmedCss = _shadowCss.callMethod('shimCssText', [css, selector]); + return "/* Shimmed css for <$selector> from $cssUrl */\n$shimmedCss"; + } + + /** + * Because this code uses `strictStyling` for the polymer css shim, it is required to add the + * custom element’s name as an attribute on all DOM nodes in the shadowRoot (e.g. ). + * + * See http://www.polymer-project.org/docs/polymer/styling.html#strictstyling + */ + void shimShadowDom(dom.Element root, String selector) { + if (! shimRequired) return; + + _addAttributeToAllElements(root, selector); + } +} + +@Injectable() +class DefaultPlatformShim implements WebPlatformShim { + bool get shimRequired => true; + + String shimCss(String css, { String selector, String cssUrl }) { + final shimmedCss = cssShim.shimCssText(css, selector); + return "/* Shimmed css for <$selector> from $cssUrl */\n$shimmedCss"; + } + + void shimShadowDom(dom.Element root, String selector) { + _addAttributeToAllElements(root, selector); + } +} + +void _addAttributeToAllElements(dom.Element root, String attr) { + // This adds an empty attribute with the name of the component tag onto + // each element in the shadow root. + // + // TODO: Remove the try-catch once https://github.com/angular/angular.dart/issues/1189 is fixed. + try { + root.querySelectorAll("*").forEach((n) => n.attributes[attr] = ""); + } catch (e, s) { + _log.warning("WARNING: Failed to set up Shadow DOM shim for $attr.\n$e\n$s"); + } +} + +class ShimmingViewCache implements ViewCache { + final ViewCache cache; + final String selector; + final WebPlatformShim platformShim; + + LruCache get viewFactoryCache => cache.viewFactoryCache; + Http get http => cache.http; + TemplateCache get templateCache => cache.templateCache; + Compiler get compiler => cache.compiler; + dom.NodeTreeSanitizer get treeSanitizer => cache.treeSanitizer; + + ShimmingViewCache(this.cache, this.selector, this.platformShim); + + ViewFactory fromHtml(String html, DirectiveMap directives) { + if (!platformShim.shimRequired) return cache.fromHtml(html, directives); + + ViewFactory viewFactory = viewFactoryCache.get(_cacheKey(html)); + if (viewFactory != null) { + return viewFactory; + } else { + // This MUST happen before the compiler is called so that every dom + // element gets touched before the compiler removes them for + // transcluding directives like ng-if. + return viewFactoryCache.put(_cacheKey(html), _createViewFactory(html, directives)); + } + } + + async.Future fromUrl(String url, DirectiveMap directives) { + if (!platformShim.shimRequired) return cache.fromUrl(url, directives); + + ViewFactory viewFactory = viewFactoryCache.get(url); + if (viewFactory != null) { + return new async.Future.value(viewFactory); + } else { + return http.get(url, cache: templateCache).then((resp) => + viewFactoryCache.put(_cacheKey(url), fromHtml(resp.responseText, directives))); + } + } + + ViewFactory _createViewFactory(String html, DirectiveMap directives) { + var div = new dom.DivElement(); + div.setInnerHtml(html, treeSanitizer: treeSanitizer); + platformShim.shimShadowDom(div, selector); + return compiler(div.nodes, directives); + } + + /** + * By adding a comment with the tag name we ensure the cached resource is + * unique per selector name when used as a key in the view factory cache. + */ + String _cacheKey(String s) => "$s"; +} diff --git a/lib/mock/mock_cache_register.dart b/lib/mock/mock_cache_register.dart new file mode 100644 index 000000000..352f30cf0 --- /dev/null +++ b/lib/mock/mock_cache_register.dart @@ -0,0 +1,10 @@ +part of angular.mock; + +/** + * This is a null implementation of CacheRegister used in tests. + */ +class MockCacheRegister implements CacheRegister { + void registerCache(String name, cache) {} + List get stats {} + void clear([String name]) {} +} \ No newline at end of file diff --git a/lib/mock/mock_platform.dart b/lib/mock/mock_platform.dart deleted file mode 100644 index 5d6570b83..000000000 --- a/lib/mock/mock_platform.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of angular.mock; - -/** - * The mock platform exists to smooth out browser differences for tests that - * do not wish to take browser variance into account. This mock, for most cases, - * will cause tests to behave according to the most recent spec. - */ -class MockWebPlatform implements WebPlatform { - bool get cssShimRequired => false; - bool get shadowDomShimRequired => false; - - String shimCss(String css, { String selector, String cssUrl }) { - return css; - } - - void shimShadowDom(Element root, String selector) {} -} \ No newline at end of file diff --git a/lib/mock/mock_platform_shim.dart b/lib/mock/mock_platform_shim.dart new file mode 100644 index 000000000..e58476ff0 --- /dev/null +++ b/lib/mock/mock_platform_shim.dart @@ -0,0 +1,20 @@ +part of angular.mock; + +/** + * The mock platform exists to smooth out browser differences for tests that + * do not wish to take browser variance into account. This mock provides null + * implementations of all operations, but they can be overwritten if needed. + */ +class MockWebPlatformShim implements PlatformJsBasedShim, DefaultPlatformShim { + bool shimRequired = false; + + Function cssCompiler = (css, {String selector}) => css; + Function shimDom = (root, String selector) {}; + + String shimCss(String css, { String selector, String cssUrl }) => + cssCompiler(css, selector: selector); + + void shimShadowDom(Element root, String selector) { + shimDom(root, selector); + } +} \ No newline at end of file diff --git a/lib/mock/module.dart b/lib/mock/module.dart index 6978540d4..0d59db42b 100644 --- a/lib/mock/module.dart +++ b/lib/mock/module.dart @@ -23,6 +23,7 @@ import 'package:angular/core/module_internal.dart'; import 'package:angular/core_dom/module_internal.dart'; import 'package:angular/core_dom/directive_injector.dart'; import 'package:angular/core/parser/parser.dart'; +import 'package:angular/cache/module.dart'; import 'package:angular/mock/static_keys.dart'; import 'package:di/di.dart'; import 'package:mock/mock.dart'; @@ -38,8 +39,9 @@ part 'exception_handler.dart'; part 'log.dart'; part 'probe.dart'; part 'test_bed.dart'; -part 'mock_platform.dart'; +part 'mock_platform_shim.dart'; part 'mock_window.dart'; +part 'mock_cache_register.dart'; /** * Use in addition to [AngularModule] in your tests. @@ -49,7 +51,7 @@ part 'mock_window.dart'; * - [TestBed] * - [Probe] * - [MockHttpBackend] instead of [HttpBackend] - * - [MockWebPlatform] instead of [WebPlatform] + * - [MockWebPlatformShim] instead of [WebPlatformShim] * - [Logger] * - [RethrowExceptionHandler] instead of [ExceptionHandler] * - [VmTurnZone] which displays errors to console; @@ -61,6 +63,7 @@ class AngularMockModule extends Module { bind(Probe); bind(Logger); bind(MockHttpBackend); + bind(CacheRegister, toImplementation: MockCacheRegister); bind(Element, toValue: document.body); bind(Node, toValue: document.body); bind(HttpBackend, toInstanceOf: MOCK_HTTP_BACKEND_KEY); @@ -69,8 +72,8 @@ class AngularMockModule extends Module { ..onError = (e, s, LongStackTrace ls) => dump('EXCEPTION: $e\n$s\n$ls'); }, inject: []); bind(Window, toImplementation: MockWindow); - var mockPlatform = new MockWebPlatform(); - bind(MockWebPlatform, toValue: mockPlatform); - bind(WebPlatform, toValue: mockPlatform); + bind(MockWebPlatformShim); + bind(PlatformJsBasedShim, toInstanceOf: MockWebPlatformShim); + bind(DefaultPlatformShim, toInstanceOf: MockWebPlatformShim); } } diff --git a/lib/utils.dart b/lib/utils.dart index 85762086b..dbaa06ac3 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,5 +1,7 @@ library angular.util; +import 'dart:async'; + bool toBool(x) { if (x is bool) return x; if (x is num) return x != 0; @@ -124,4 +126,12 @@ final Set RESERVED_WORDS = new Set.from(const [ bool isNaN(Object o) => o is num && o.isNaN; /// Returns true iff o1 == o2 or both are [double.NAN]. -bool eqOrNaN(Object o1, Object o2) => o1 == o2 || (isNaN(o1) && isNaN(o2)); \ No newline at end of file +bool eqOrNaN(Object o1, Object o2) => o1 == o2 || (isNaN(o1) && isNaN(o2)); + +/// Merges two futures of iterables into one. +Future mergeFutures(Future f1, Future f2) { + return Future.wait([f1, f2]).then((twoLists) { + assert(twoLists.length == 2); + return []..addAll(twoLists[0])..addAll(twoLists[1]); + }); +} diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index f6dc8e263..dde94b556 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -56,9 +56,9 @@ if [[ $TESTS == "dart2js" ]]; then echo '-----------------------------------' cd $NGDART_BASE_DIR/example checkSize build/web/animation.dart.js 208021 - checkSize build/web/bouncing_balls.dart.js 202325 + checkSize build/web/bouncing_balls.dart.js 212482 checkSize build/web/hello_world.dart.js 210000 - checkSize build/web/todo.dart.js 203121 + checkSize build/web/todo.dart.js 213352 if ((SIZE_TOO_BIG_COUNT > 0)); then exit 1 else diff --git a/test/cache/cache_register_spec.dart b/test/cache/cache_register_spec.dart index a797385ea..04d4f23b6 100644 --- a/test/cache/cache_register_spec.dart +++ b/test/cache/cache_register_spec.dart @@ -3,6 +3,10 @@ library cache_register_spec; import '../_specs.dart'; main() => describe('CacheRegister', () { + beforeEachModule((Module m) { + m.bind(CacheRegister); + }); + it('should clear caches', (CacheRegister register) { var map = {'a': 2}; var map2 = {'b': 3}; diff --git a/test/cache/js_cache_register_spec.dart b/test/cache/js_cache_register_spec.dart index 667456eec..46e748e5a 100644 --- a/test/cache/js_cache_register_spec.dart +++ b/test/cache/js_cache_register_spec.dart @@ -5,6 +5,10 @@ import 'dart:js' as js; import 'package:angular/application_factory.dart'; main() => describe('JsCacheRegister', () { + beforeEachModule((Module m) { + m.bind(CacheRegister); + }); + s() => js.context['ngCaches']['sizes'].apply([]); // Create some caches in the system diff --git a/test/core/component_css_loader_spec.dart b/test/core/component_css_loader_spec.dart new file mode 100644 index 000000000..adc6ad206 --- /dev/null +++ b/test/core/component_css_loader_spec.dart @@ -0,0 +1,92 @@ +library component_css_loader; + +import '../_specs.dart'; +import 'dart:html' as dom; + +void main() { + describe("ComponentCssLoader", () { + ComponentCssLoader loader; + + beforeEach((Http http, TemplateCache tc, MockWebPlatformShim shim, + ComponentCssRewriter ccr, dom.NodeTreeSanitizer ts) { + loader = new ComponentCssLoader(http, tc, shim, ccr, ts, {}); + }); + + afterEach((MockHttpBackend backend) { + backend.verifyNoOutstandingExpectation(); + backend.verifyNoOutstandingRequest(); + }); + + it('should return created style elements', async((MockHttpBackend backend) { + backend..expectGET('simple1.css').respond(200, '.hello1{}'); + backend..expectGET('simple2.css').respond(200, '.hello2{}'); + + final res = loader("tag", ['simple1.css', 'simple2.css']); + + backend.flush(); + microLeap(); + + res.then((elements) { + expect(elements[0]).toHaveText(".hello1{}"); + expect(elements[1]).toHaveText(".hello2{}"); + }); + })); + + it('should ignore CSS load errors ', async((MockHttpBackend backend) { + backend..expectGET('simple.css').respond(500, 'some error'); + + final res = loader("tag", ['simple.css']); + + backend.flush(); + microLeap(); + + res.then((elements) { + expect(elements.first).toHaveText('/* HTTP 500: some error */'); + }); + })); + + it('should use same style for the same tag', async(( + MockHttpBackend backend, MockWebPlatformShim shim) { + backend..expectGET('simple.css').respond(200, '.hello{}'); + shim.cssCompiler = (css, {selector}) => "$selector - $css"; + + final f1 = loader("tag", ['simple.css']); + + backend.flush(); + microLeap(); + + final f2 = loader("tag", ['simple.css']); + microLeap(); + + f1.then((el) { + expect(el[0]).toHaveText("tag - .hello{}"); + }); + + f2.then((el) { + expect(el[0]).toHaveText("tag - .hello{}"); + }); + })); + + it('should create new style for every tag', async(( + MockHttpBackend backend, MockWebPlatformShim shim) { + backend..expectGET('simple.css').respond(200, '.hello{}'); + shim.cssCompiler = (css, {selector}) => "$selector - $css"; + + final f1 = loader("tag1", ['simple.css']); + + backend.flush(); + microLeap(); + + final f2 = loader("tag2", ['simple.css']); + microLeap(); + + f1.then((el) { + expect(el[0]).toHaveText("tag1 - .hello{}"); + }); + + f2.then((el) { + expect(el[0]).toHaveText("tag2 - .hello{}"); + }); + })); + }); +} diff --git a/test/core/templateurl_spec.dart b/test/core/templateurl_spec.dart index 450d34e0d..96f864f5c 100644 --- a/test/core/templateurl_spec.dart +++ b/test/core/templateurl_spec.dart @@ -1,46 +1,74 @@ -library templateurl_spec; +library component_template_and_css_spec; import '../_specs.dart'; +import 'dart:html' as dom; +import 'dart:async'; @Component( selector: 'simple-url', templateUrl: 'simple.html') -class SimpleUrlComponent { +class _SimpleUrlComponent { } @Component( selector: 'html-and-css', templateUrl: 'simple.html', cssUrl: 'simple.css') -class HtmlAndCssComponent { +class _HtmlAndCssComponent { } @Component( - selector: 'html-and-css', - templateUrl: 'simple.html', - cssUrl: const ['simple.css', 'another.css']) -class HtmlAndMultipleCssComponent { + selector: 'only-css', + cssUrl: 'simple.css') +class _OnlyCssComponent { } @Component( - selector: 'inline-with-css', - template: '
inline!
', - cssUrl: 'simple.css') -class InlineWithCssComponent { + selector: 'transcluding', + cssUrl: 'transcluding.css', + useShadowDom: false +) +class _TranscludingComponent { } @Component( - selector: 'only-css', - cssUrl: 'simple.css') -class OnlyCssComponent { + selector: 'shadow-with-transcluding', + template: "", + cssUrl: 'shadow.css', + useShadowDom: true +) +class _ShadowComponentWithTranscludingComponent { } + class PrefixedUrlRewriter extends UrlRewriter { call(url) => "PREFIX:$url"; } +void shadowDomAndTranscluding(name, fn) { + describe(name, (){ + describe('transcluding components', () { + beforeEachModule((Module m) { + m.bind(ComponentFactory, toImplementation: TranscludingComponentFactory); + }); + fn(); + }); + + describe('shadow dom components', () { + beforeEachModule((Module m) { + m.bind(ComponentFactory, toImplementation: ShadowDomComponentFactory); + }); + fn(); + }); + }); +} + void main() { - describe('template url', () { + describe('template and css loading', () { + TestBed _; + + beforeEach((TestBed tb) => _ = tb); + afterEach((MockHttpBackend backend) { backend.verifyNoOutstandingExpectation(); backend.verifyNoOutstandingRequest(); @@ -49,234 +77,141 @@ void main() { describe('loading with http rewriting', () { beforeEachModule((Module module) { module - ..bind(HtmlAndCssComponent) + ..bind(_HtmlAndCssComponent) ..bind(UrlRewriter, toImplementation: PrefixedUrlRewriter); }); - it('should use the UrlRewriter for both HTML and CSS URLs', async( - (Http http, Compiler compile, Scope rootScope, Logger log, - Injector injector, VmTurnZone zone, MockHttpBackend backend, - DirectiveMap directives) { + it('should use the UrlRewriter for both HTML and CSS URLs', async(( + MockHttpBackend backend) { backend - ..whenGET('PREFIX:simple.html').respond('
Simple!
') + ..whenGET('PREFIX:simple.html').respond('
Simple!
') ..whenGET('PREFIX:simple.css').respond('.hello{}'); - var element = e('
ignore
'); - zone.run(() { - compile([element], directives)(rootScope, null, [element]); - }); + var element = _.compile('
ignore
'); backend.flush(); microLeap(); expect(element).toHaveText('.hello{}Simple!'); expect(element.children[0].shadowRoot).toHaveHtml( - '
Simple!
' + '
Simple!
' ); })); }); - describe('async template loading', () { + shadowDomAndTranscluding('template loading', () { beforeEachModule((Module module) { module ..bind(LogAttrDirective) - ..bind(SimpleUrlComponent) - ..bind(HtmlAndCssComponent) - ..bind(OnlyCssComponent) - ..bind(InlineWithCssComponent); + ..bind(_SimpleUrlComponent); }); - it('should replace element with template from url', async( - (Http http, Compiler compile, Scope rootScope, Logger log, - Injector injector, MockHttpBackend backend, DirectiveMap directives) { + it('should replace element with template from url', async(( + Logger log, MockHttpBackend backend) { backend.expectGET('simple.html').respond(200, '
Simple!
'); - var element = es('
ignore
'); - compile(element, directives)(rootScope, null, element); + var element = _.compile('
ignore
'); microLeap(); backend.flush(); microLeap(); - expect(element[0]).toHaveText('Simple!'); - rootScope.apply(); + expect(element).toHaveText('Simple!'); + _.rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); })); - it('should load template from URL once', async( - (Http http, Compiler compile, Scope rootScope, Logger log, - Injector injector, MockHttpBackend backend, DirectiveMap directives) { + it('should load template from URL once', async(( + Logger log, MockHttpBackend backend) { backend.whenGET('simple.html').respond(200, '
Simple!
'); - var element = es( + var element = _.compile( '
' 'ignore' 'ignore' '
'); - compile(element, directives)(rootScope, null, element); microLeap(); backend.flush(); microLeap(); - expect(element.first).toHaveText('Simple!Simple!'); - rootScope.apply(); + expect(element).toHaveText('Simple!Simple!'); + _.rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; LOG; SIMPLE; SIMPLE'); })); - - it('should load a CSS file into a style', async( - (Http http, Compiler compile, Scope rootScope, Logger log, - Injector injector, MockHttpBackend backend, DirectiveMap directives) { - backend - ..expectGET('simple.css').respond(200, '.hello{}') - ..expectGET('simple.html').respond(200, '
Simple!
'); - - var element = e('
ignore
'); - compile([element], directives)(rootScope, null, [element]); - - microLeap(); - backend.flush(); - microLeap(); - - expect(element).toHaveText('.hello{}Simple!'); - expect(element.children[0].shadowRoot).toHaveHtml( - '
Simple!
' - ); - rootScope.apply(); - // Note: There is no ordering. It is who ever comes off the wire first! - expect(log.result()).toEqual('LOG; SIMPLE'); - })); - - it('should load a CSS file with a \$template', async( - (Http http, Compiler compile, Scope rootScope, Injector injector, - MockHttpBackend backend, DirectiveMap directives) { - var element = es('
ignore
'); - backend.expectGET('simple.css').respond(200, '.hello{}'); - compile(element, directives)(rootScope, null, element); - - microLeap(); - backend.flush(); - microLeap(); - expect(element[0]).toHaveText('.hello{}inline!'); - })); - - it('should ignore CSS load errors ', async( - (Http http, Compiler compile, Scope rootScope, Injector injector, - MockHttpBackend backend, DirectiveMap directives) { - var element = es('
ignore
'); - backend.expectGET('simple.css').respond(500, 'some error'); - compile(element, directives)(rootScope, null, element); - - microLeap(); - backend.flush(); - microLeap(); - expect(element.first).toHaveText( - '/*\n' - 'HTTP 500: some error\n' - '*/\n' - 'inline!'); - })); - - it('should load a CSS with no template', async( - (Http http, Compiler compile, Scope rootScope, Injector injector, - MockHttpBackend backend, DirectiveMap directives) { - var element = es('
ignore
'); - backend.expectGET('simple.css').respond(200, '.hello{}'); - compile(element, directives)(rootScope, null, element); - - microLeap(); - backend.flush(); - microLeap(); - expect(element[0]).toHaveText('.hello{}'); - })); - - it('should load the CSS before the template is loaded', async( - (Http http, Compiler compile, Scope rootScope, Injector injector, - MockHttpBackend backend, DirectiveMap directives) { - backend - ..expectGET('simple.css').respond(200, '.hello{}') - ..expectGET('simple.html').respond(200, '
Simple!
'); - - var element = es('ignore'); - compile(element, directives)(rootScope, null, element); - - microLeap(); - backend.flush(); - microLeap(); - expect(element.first).toHaveText('.hello{}Simple!'); - })); }); - describe('multiple css loading', () { + + describe('css loading (shadow dom components)', () { beforeEachModule((Module module) { module - ..bind(LogAttrDirective) - ..bind(HtmlAndMultipleCssComponent); + ..bind(LogAttrDirective) + ..bind(_HtmlAndCssComponent) + ..bind(_OnlyCssComponent); }); - it('should load multiple CSS files into a style', async( - (Http http, Compiler compile, Scope rootScope, Logger log, - Injector injector, MockHttpBackend backend, DirectiveMap directives) { + it("should append the component's CSS to the shadow root", async(( + Logger log, MockHttpBackend backend) { backend ..expectGET('simple.css').respond(200, '.hello{}') - ..expectGET('another.css').respond(200, '.world{}') ..expectGET('simple.html').respond(200, '
Simple!
'); - var element = e('
ignore
'); - compile([element], directives)(rootScope, null, [element]); + var element = _.compile('
ignore
'); microLeap(); backend.flush(); microLeap(); - expect(element).toHaveText('.hello{}.world{}Simple!'); + expect(element).toHaveText('.hello{}Simple!'); expect(element.children[0].shadowRoot).toHaveHtml( - '
Simple!
' + '
Simple!
' ); - rootScope.apply(); + _.rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); })); }); - describe('style cache', () { + describe('css loading (transcluding components)', () { beforeEachModule((Module module) { module - ..bind(HtmlAndCssComponent) - ..bind(TemplateCache, toValue: new TemplateCache(capacity: 0)); + ..bind(_TranscludingComponent) + ..bind(_ShadowComponentWithTranscludingComponent); }); - it('should load css from the style cache for the second component', async( - (Http http, Compiler compile, MockHttpBackend backend, RootScope rootScope, - DirectiveMap directives, Injector injector) { - backend - ..expectGET('simple.css').respond(200, '.hello{}') - ..expectGET('simple.html').respond(200, '
Simple!
'); + afterEach(() { + document.head.querySelectorAll("style").forEach((s) => s.remove()); + }); - var element = e('
ignore
'); - compile([element], directives)(rootScope, null, [element]); + it("should append the component's CSS to the closest shadow root", async(( + MockHttpBackend backend) { + backend + ..whenGET('shadow.css').respond(200, '.shadow{}') + ..whenGET('transcluding.css').respond(200, '.transcluding{}'); - microLeap(); - backend.flush(); - microLeap(); + final e = _.compile('
'); + backend.flush(1); _.rootScope.apply(); microLeap(); + backend.flush(1); _.rootScope.apply(); microLeap(); - expect(element.children[0].shadowRoot).toHaveHtml( - '
Simple!
' + expect(e.children[0].shadowRoot).toHaveHtml( + '' ); + })); - var element2 = e('
ignore
'); - compile([element2], directives)(rootScope, null, [element2]); + it("should append the component's CSS to head when no enclosing shadow roots", async(( + MockHttpBackend backend) { + backend + ..whenGET('transcluding.css').respond(200, '.transcluding{}'); - microLeap(); + final e = _.compile('
'); + backend.flush(); _.rootScope.apply(); microLeap(); - expect(element2.children[0].shadowRoot).toHaveHtml( - '
Simple!
' - ); + expect(document.head.text).toContain('.transcluding{}'); })); }); }); diff --git a/test/core_dom/css_shim_spec.dart b/test/core_dom/css_shim_spec.dart new file mode 100644 index 000000000..cc7179b55 --- /dev/null +++ b/test/core_dom/css_shim_spec.dart @@ -0,0 +1,83 @@ +library css_shim_spec; + +import '../_specs.dart'; +import 'package:angular/core_dom/css_shim.dart'; +import 'dart:html' as dom; + +main() { + describe("cssShim", () { + s(String css, String tag) => + shimCssText(css, tag).replaceAll("\n", ""); + + it("should handle empty string", () { + expect(s("", "a")).toEqual(""); + }); + + it("should add an attribute to every rule", () { + final css = "one {color: red;}two {color: red;}"; + + final expected = "one[a] {color: red;}two[a] {color: red;}"; + + expect(s(css, "a")).toEqual(expected); + }); + + it("should hanlde invalid css", () { + final css = "one {color: red;}garbage"; + + final expected = "one[a] {color: red;}"; + + expect(s(css, "a")).toEqual(expected); + }); + + it("should add an attribute to every selector", () { + final css = "one, two {color: red;}"; + + final expected = "one[a], two[a] {color: red;}"; + + expect(s(css, "a")).toEqual(expected); + }); + + it("should handle media rules", () { + final css = "@media screen and (max-width: 800px) {div {font-size: 50px;}}"; + + final 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", () { + final css = "@media screen and (max-width: 800px) {div {font-size: 50px;}} div {}"; + + final 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(r'one[attr$="value"] {}', "a")).toEqual(r'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 insert directives", () { + var css = s("polyfill-next-selector {content: 'x > y'} z {}", "a"); + expect(css).toEqual('x[a]>y[a] {}'); + }); + }); +} \ No newline at end of file diff --git a/test/core_dom/default_platform_shim_spec.dart b/test/core_dom/default_platform_shim_spec.dart new file mode 100644 index 000000000..66c68c820 --- /dev/null +++ b/test/core_dom/default_platform_shim_spec.dart @@ -0,0 +1,87 @@ +library angular.dom.default_platform_shim_spec; + +import '../_specs.dart'; + +import 'dart:js' as js; + +main() { + describe("DefaultPlatformShim", () { + final shim = new DefaultPlatformShim(); + + describe("shimCss", () { + it("should shim the given css", () { + final shimmed = shim.shimCss("a{color: red;}", selector: "SELECTOR", cssUrl: 'URL'); + + expect(shimmed).toContain("Shimmed css for from URL"); + }); + }); + + describe("shimShadowDom", () { + it("add an attribute to all element in the dom subtree", () { + final root = e("
"); + + shim.shimShadowDom(root, "selector"); + + expect(root).toHaveHtml(''); + }); + + // TODO: Remove the test once https://github.com/angular/angular.dart/issues/1300 is fixed + it("should not crash with an invalid selector; but wont work either", () { + final root = e("
"); + + shim.shimShadowDom(root, "c[a]"); + + expect(root).toHaveHtml(''); + }); + }); + + describe("Integration Test", () { + beforeEachModule((Module module) { + module + ..bind(ComponentFactory, toImplementation: TranscludingComponentFactory) + ..bind(DefaultPlatformShim) + ..bind(_WebPlatformTestComponent); + }); + + it('should scope styles to shadow dom across browsers.', async(( + TestBed _, MockHttpBackend backend) { + backend + ..expectGET('style.css').respond(200, 'span { background-color: red; }') + ..expectGET('template.html').respond(200, 'foo'); + + Element element = _.compile(''); + + microLeap(); + backend.flush(); + microLeap(); + + try { + document.body.append(element); + microLeap(); + + // Outer span should not be styled. + expect(element.getComputedStyle().backgroundColor) + .not.toEqual("rgb(255, 0, 0)"); + + // "Shadow root" should be styled. + expect(element.children[0].querySelector("span") + .getComputedStyle().backgroundColor).toEqual("rgb(255, 0, 0)"); + + } finally { + element.remove(); + } + })); + }); + }); +} + +@Component( + selector: "test-wptc", + publishAs: "ctrl", + templateUrl: "template.html", + cssUrl: "style.css") +class _WebPlatformTestComponent { +} + + + diff --git a/test/core_dom/event_handler_spec.dart b/test/core_dom/event_handler_spec.dart index 68a6784a6..acdae9a1e 100644 --- a/test/core_dom/event_handler_spec.dart +++ b/test/core_dom/event_handler_spec.dart @@ -1,4 +1,4 @@ -library event_handler_spec; +library shadow_boundary_spec; import '../_specs.dart'; @@ -109,4 +109,4 @@ main() { expect(fooScope.context['ctrl'].invoked).toEqual(true); })); }); -} +} \ No newline at end of file diff --git a/test/core_dom/web_platform_spec.dart b/test/core_dom/platform_js_based_shim_spec.dart similarity index 81% rename from test/core_dom/web_platform_spec.dart rename to test/core_dom/platform_js_based_shim_spec.dart index 17c96f68c..0470aed73 100644 --- a/test/core_dom/web_platform_spec.dart +++ b/test/core_dom/platform_js_based_shim_spec.dart @@ -1,23 +1,22 @@ -library angular.dom.platform_spec; +library angular.dom.platform_js_based_shim_spec; import '../_specs.dart'; import 'dart:js' as js; main() { - describe('WebPlatform', () { + describe('PlatformJsBasedShim', () { beforeEachModule((Module module) { module ..bind(_WebPlatformTestComponent) - ..bind(_WebPlatformTestComponentWithAttribute) ..bind(_InnerComponent) ..bind(_OuterComponent) - ..bind(WebPlatform, toValue: new WebPlatform()); + ..bind(PlatformJsBasedShim, toValue: new PlatformJsBasedShim()); }); it('should scope styles to shadow dom across browsers.', - async((TestBed _, MockHttpBackend backend, WebPlatform platform) { + async((TestBed _, MockHttpBackend backend) { backend ..expectGET('style.css').respond(200, 'span { background-color: red; ' @@ -55,26 +54,8 @@ main() { } })); - it('should not crash with an attribute selector; but wont work either..', - async((TestBed _, MockHttpBackend backend, WebPlatform platform) { - - backend - ..expectGET('style.css').respond(200, 'span { background-color: red; ' - '}') - ..expectGET('template.html').respond(200, 'foo'); - - Element element = e('ignore' - ''); - - _.compile(element); - - microLeap(); - backend.flush(); - microLeap(); - })); - it('should scope :host styles to the primary element.', - async((TestBed _, MockHttpBackend backend, WebPlatform platform) { + async((TestBed _, MockHttpBackend backend) { backend ..expectGET('style.css').respond(200, ':host {' @@ -107,7 +88,7 @@ main() { // safari and dartium browsers. Add back into the list of tests when firefox // pushes new version(s). xit('should scope ::content rules to component content.', - async((TestBed _, MockHttpBackend backend, WebPlatform platform) { + async((TestBed _, MockHttpBackend backend) { backend ..expectGET('style.css').respond(200, @@ -145,7 +126,7 @@ main() { // NOTE: Chrome 34 does not work with this test. Uncomment when the dartium // base Chrome version is > 34 xit('should style into child shadow dom with ::shadow.', - async((TestBed _, MockHttpBackend backend, WebPlatform platform) { + async((TestBed _, MockHttpBackend backend) { backend ..expectGET('outer-style.css').respond(200, 'my-inner::shadow .foo {' @@ -195,14 +176,6 @@ main() { class _WebPlatformTestComponent { } -@Component( - selector: "test-wptca[a]", - publishAs: "ctrl", - templateUrl: "template.html", - cssUrl: "style.css") -class _WebPlatformTestComponentWithAttribute { -} - @Component( selector: "my-inner", publishAs: "ctrl", diff --git a/test/core_dom/shimming_view_cache_spec.dart b/test/core_dom/shimming_view_cache_spec.dart new file mode 100644 index 000000000..f240e4bd8 --- /dev/null +++ b/test/core_dom/shimming_view_cache_spec.dart @@ -0,0 +1,106 @@ +library angular.dom.shimming_view_cache_spec; + +import '../_specs.dart'; +import 'package:angular/core_dom/css_shim.dart'; + +main() { + describe("ShimmingViewCache", () { + ShimmingViewCache cache; + MockWebPlatformShim platformShim; + MockHttpBackend backend; + Injector inj; + TestBed _; + + beforeEach((Injector _inj, TestBed tb, MockHttpBackend _backend, MockWebPlatformShim _platformShim) { + _ = tb; + inj = _inj; + backend = _backend; + + platformShim = _platformShim; + platformShim.shimDom = (root, selector) { + root.innerHtml = "SHIMMED"; + }; + + cache = new ShimmingViewCache(inj.get(ViewCache), "selector", platformShim); + }); + + describe("fromHtml", () { + fromHtml(ViewCache cache, String html) { + final viewFactory = cache.fromHtml(html, inj.get(DirectiveMap)); + return viewFactory(_.rootScope, inj.get(DirectiveInjector)); + } + + describe("shim is not required", () { + it("should delegate to the decorated cache", () { + platformShim.shimRequired = false; + expect(fromHtml(cache, "HTML")).toHaveText("HTML"); + }); + }); + + describe("shim is required", () { + beforeEach(() { + platformShim.shimRequired = true; + }); + + it("should shim the dom", () { + expect(fromHtml(cache, "HTML")).toHaveText("SHIMMED"); + }); + + it("uses uniq cache key per selector", () { + final cache2 = new ShimmingViewCache(inj.get(ViewCache), "selector2", platformShim); + + fromHtml(cache, "HTML"); + fromHtml(cache2, "HTML"); + + expect(cache.viewFactoryCache.size).toEqual(2); + }); + }); + }); + + describe("fromUrl", () { + beforeEach(() { + backend.whenGET("URL").respond(200, "HTML"); + }); + + fromUrl(ViewCache cache, String url) { + final f = cache.fromUrl(url, inj.get(DirectiveMap)); + + if (backend.responses.isNotEmpty) backend.flush(); + microLeap(); + + return f.then((vf) => vf(_.rootScope, inj.get(DirectiveInjector))); + } + + describe("shim is not required", () { + it("should delegate to the decorated cache", async(() { + platformShim.shimRequired = false; + + return fromUrl(cache, "URL").then((view) { + expect(view).toHaveText("HTML"); + }); + })); + }); + + describe("shim is required", () { + beforeEach(() { + platformShim.shimRequired = true; + }); + + it("should shim the dom", async(() { + return fromUrl(cache, "URL").then((view) { + expect(view).toHaveText("SHIMMED"); + }); + })); + + it("uses uniq cache key per selector", async(() { + final cache2 = new ShimmingViewCache(inj.get(ViewCache), "selector2", platformShim); + + fromUrl(cache, "URL"); + fromUrl(cache2, "URL"); + + expect(cache.viewFactoryCache.size).toEqual(4); //2 html, 2 url + })); + }); + }); + }); +} \ No newline at end of file diff --git a/test/directive/ng_base_css_spec.dart b/test/directive/ng_base_css_spec.dart index bc56214be..b60275794 100644 --- a/test/directive/ng_base_css_spec.dart +++ b/test/directive/ng_base_css_spec.dart @@ -116,4 +116,45 @@ main() => describe('NgBaseCss', () { ); })); }); + + describe('transcluding components', () { + beforeEachModule((Module m) { + m.bind(ComponentFactory, toImplementation: TranscludingComponentFactory); + }); + + afterEach(() { + document.head.querySelectorAll("style").forEach((t) => t.remove()); + }); + + it('should load css urls from ng-base-css', async((TestBed _, MockHttpBackend backend) { + backend + ..whenGET('simple.css').respond(200, '.simple{}') + ..whenGET('simple.html').respond(200, '
Simple!
') + ..whenGET('base.css').respond(200, '.base{}'); + + var element = _.compile('
ignore
'); + + microLeap(); + backend.flush(); + microLeap(); + + final styleTags = document.head.querySelectorAll("style"); + expect(styleTags[styleTags.length - 2]).toHaveText(".base{}"); + expect(styleTags.last).toHaveText(".simple{}"); + })); + + it('should respect useNgBaseCss', async((TestBed _, MockHttpBackend backend) { + backend + ..whenGET('simple.css').respond(200, '.simple{}') + ..whenGET('simple.html').respond(200, '
Simple!
'); + + var element = _.compile('
ignore
'); + + microLeap(); + backend.flush(); + microLeap(); + + expect(document.head.text).not.toContain(".base{}"); + })); + }); });