diff --git a/lib/core/annotation_src.dart b/lib/core/annotation_src.dart index 1c1796d12..7f4802f9e 100644 --- a/lib/core/annotation_src.dart +++ b/lib/core/annotation_src.dart @@ -308,6 +308,13 @@ class Component extends Directive { @deprecated final String publishAs; + /** + * If set to true, this component will always use shadow DOM. + * If set to false, this component will never use shadow DOM. + * If unset, the compiler's default construction strategy will be used + */ + final bool useShadowDom; + const Component({ this.template, this.templateUrl, @@ -320,7 +327,8 @@ class Component extends Directive { selector, visibility, exportExpressions, - exportExpressionAttrs}) + exportExpressionAttrs, + this.useShadowDom}) : _cssUrls = cssUrl, _applyAuthorStyles = applyAuthorStyles, _resetStyleInheritance = resetStyleInheritance, diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index 13035f6fc..dd01377ac 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -14,9 +14,13 @@ class TemplateElementBinder extends ElementBinder { return _directiveCache = [template]; } - TemplateElementBinder(_perf, _expando, _parser, _componentFactory, this.template, this.templateBinder, + TemplateElementBinder(perf, expando, parser, componentFactory, + transcludingComponentFactory, shadowDomComponentFactory, + this.template, this.templateBinder, onEvents, bindAttrs, childMode) - : super(_perf, _expando, _parser, _componentFactory, null, null, onEvents, bindAttrs, childMode); + : super(perf, expando, parser, componentFactory, + transcludingComponentFactory, shadowDomComponentFactory, + null, null, onEvents, bindAttrs, childMode); String toString() => "[TemplateElementBinder template:$template]"; @@ -41,7 +45,11 @@ class ElementBinder { final Profiler _perf; final Expando _expando; final Parser _parser; + + // The default component factory final ComponentFactory _componentFactory; + final TranscludingComponentFactory _transcludingComponentFactory; + final ShadowDomComponentFactory _shadowDomComponentFactory; final Map onEvents; final Map bindAttrs; @@ -53,7 +61,11 @@ class ElementBinder { // Can be either COMPILE_CHILDREN or IGNORE_CHILDREN final String childMode; - ElementBinder(this._perf, this._expando, this._parser, this._componentFactory, this.component, this.decorators, + ElementBinder(this._perf, this._expando, this._parser, + this._componentFactory, + this._transcludingComponentFactory, + this._shadowDomComponentFactory, + this.component, this.decorators, this.onEvents, this.bindAttrs, this.childMode); final bool hasTemplate = false; @@ -215,7 +227,16 @@ class ElementBinder { } nodesAttrsDirectives.add(ref); } else if (ref.annotation is Component) { - nodeModule.factory(ref.type, _componentFactory.call(node, ref), visibility: visibility); + var factory; + var annotation = ref.annotation as Component; + if (annotation.useShadowDom == true) { + factory = _shadowDomComponentFactory; + } else if (annotation.useShadowDom == false) { + factory = _transcludingComponentFactory; + } else { + factory = _componentFactory; + } + nodeModule.factory(ref.type, factory.call(node, ref), visibility: visibility); } else { nodeModule.type(ref.type, visibility: visibility); } diff --git a/lib/core_dom/element_binder_builder.dart b/lib/core_dom/element_binder_builder.dart index 12c4fbcb5..050c53c18 100644 --- a/lib/core_dom/element_binder_builder.dart +++ b/lib/core_dom/element_binder_builder.dart @@ -6,17 +6,22 @@ class ElementBinderFactory { final Profiler _perf; final Expando _expando; final ComponentFactory _componentFactory; + final TranscludingComponentFactory _transcludingComponentFactory; + final ShadowDomComponentFactory _shadowDomComponentFactory; - ElementBinderFactory(this._parser, this._perf, this._expando, this._componentFactory); + ElementBinderFactory(this._parser, this._perf, this._expando, this._componentFactory, + this._transcludingComponentFactory, this._shadowDomComponentFactory); // TODO: Optimize this to re-use a builder. ElementBinderBuilder builder() => new ElementBinderBuilder(this); ElementBinder binder(ElementBinderBuilder b) => new ElementBinder(_perf, _expando, _parser, _componentFactory, + _transcludingComponentFactory, _shadowDomComponentFactory, b.component, b.decorators, b.onEvents, b.bindAttrs, b.childMode); TemplateElementBinder templateBinder(ElementBinderBuilder b, ElementBinder transclude) => new TemplateElementBinder(_perf, _expando, _parser, _componentFactory, + _transcludingComponentFactory, _shadowDomComponentFactory, b.template, transclude, b.onEvents, b.bindAttrs, b.childMode); } diff --git a/lib/core_dom/module_internal.dart b/lib/core_dom/module_internal.dart index 6dd5aa5aa..245256d21 100644 --- a/lib/core_dom/module_internal.dart +++ b/lib/core_dom/module_internal.dart @@ -34,9 +34,11 @@ part 'mustache.dart'; part 'node_cursor.dart'; part 'selector.dart'; part 'shadow_dom_component_factory.dart'; +part 'shadowless_shadow_root.dart'; part 'tagging_compiler.dart'; part 'tagging_view_factory.dart'; part 'template_cache.dart'; +part 'transcluding_component_factory.dart'; part 'tree_sanitizer.dart'; part 'walking_compiler.dart'; part 'ng_element.dart'; @@ -53,7 +55,13 @@ class CoreDomModule extends Module { type(AttrMustache); type(Compiler, implementedBy: TaggingCompiler); + type(ComponentFactory, implementedBy: ShadowDomComponentFactory); + type(ShadowDomComponentFactory); + type(TranscludingComponentFactory); + type(_Content); + value(_ContentPort, null); + type(Http); type(UrlRewriter); type(HttpBackend); diff --git a/lib/core_dom/shadow_dom_component_factory.dart b/lib/core_dom/shadow_dom_component_factory.dart index cdc2daee4..edeee1618 100644 --- a/lib/core_dom/shadow_dom_component_factory.dart +++ b/lib/core_dom/shadow_dom_component_factory.dart @@ -2,6 +2,24 @@ part of angular.core.dom_internal; abstract class ComponentFactory { FactoryFn call(dom.Node node, DirectiveRef ref); + + static _viewFuture(Component component, ViewCache viewCache, DirectiveMap directives) { + if (component.template != null) { + return new async.Future.value(viewCache.fromHtml(component.template, directives)); + } else if (component.templateUrl != null) { + return viewCache.fromUrl(component.templateUrl, directives); + } + return null; + } + + static TemplateLoader _setupOnShadowDomAttach(controller, templateLoader, shadowScope) { + if (controller is ShadowRootAware) { + templateLoader.template.then((shadowDom) { + if (!shadowScope.isAttached) return; + (controller as ShadowRootAware).onShadowRoot(shadowDom); + }); + } + } } class ShadowDomComponentFactory implements ComponentFactory { @@ -12,7 +30,6 @@ class ShadowDomComponentFactory implements ComponentFactory { FactoryFn call(dom.Node node, DirectiveRef ref) { return (Injector injector) { var component = ref.annotation as Component; - Compiler compiler = injector.get(Compiler); Scope scope = injector.get(Scope); ViewCache viewCache = injector.get(ViewCache); Http http = injector.get(Http); @@ -78,13 +95,7 @@ class _ComponentFactory implements Function { } else { cssFutures.add(new async.Future.value(null)); } - var viewFuture; - if (component.template != null) { - viewFuture = new async.Future.value(viewCache.fromHtml( - component.template, directives)); - } else if (component.templateUrl != null) { - viewFuture = viewCache.fromUrl(component.templateUrl, directives); - } + var viewFuture = ComponentFactory._viewFuture(component, viewCache, directives); TemplateLoader templateLoader = new TemplateLoader( async.Future.wait(cssFutures).then((Iterable cssList) { if (cssList != null) { @@ -98,19 +109,14 @@ class _ComponentFactory implements Function { if (viewFuture != null) { return viewFuture.then((ViewFactory viewFactory) { return (!shadowScope.isAttached) ? - shadowDom : - attachViewToShadowDom(viewFactory); + shadowDom : + attachViewToShadowDom(viewFactory); }); } return shadowDom; })); controller = createShadowInjector(injector, templateLoader).get(type); - if (controller is ShadowRootAware) { - templateLoader.template.then((_) { - if (!shadowScope.isAttached) return; - (controller as ShadowRootAware).onShadowRoot(shadowDom); - }); - } + ComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); return controller; } diff --git a/lib/core_dom/shadowless_shadow_root.dart b/lib/core_dom/shadowless_shadow_root.dart new file mode 100644 index 000000000..d4994a458 --- /dev/null +++ b/lib/core_dom/shadowless_shadow_root.dart @@ -0,0 +1,12 @@ +part of angular.core.dom_internal; + +@proxy +class ShadowlessShadowRoot implements dom.ShadowRoot { + dom.Element _element; + + ShadowlessShadowRoot(this._element); + + noSuchMethod(Invocation invocation) { + throw new UnimplementedError("Not yet implemented in ShadowlessShadowRoot."); + } +} diff --git a/lib/core_dom/transcluding_component_factory.dart b/lib/core_dom/transcluding_component_factory.dart new file mode 100644 index 000000000..1de07070f --- /dev/null +++ b/lib/core_dom/transcluding_component_factory.dart @@ -0,0 +1,122 @@ +part of angular.core.dom_internal; + +@Decorator( + selector: 'content' +) +class _Content implements AttachAware, DetachAware { + final _ContentPort _port; + final dom.Element _element; + dom.Comment _beginComment; + _Content(this._port, this._element); + + attach() { + if (_port == null) return; + _beginComment = _port.content(_element); + } + + detach() { + if (_port == null) return; + _port.detachContent(_beginComment); + } +} + +class _ContentPort { + dom.Element _element; + var _childNodes = []; + + _ContentPort(this._element); + + pullNodes() { + _element.nodes.forEach((n) => _childNodes.add(n)); + _element.nodes = []; + } + + content(dom.Element elt) { + var hash = elt.hashCode; + var beginComment = new dom.Comment("content $hash"); + + if (!_childNodes.isEmpty) { + elt.parent.insertBefore(beginComment, elt); + elt.parent.insertAllBefore(_childNodes, elt); + elt.parent.insertBefore(new dom.Comment("end-content $hash"), elt); + _childNodes = []; + } + elt.remove(); + return beginComment; + } + + detachContent(dom.Node _beginComment) { + // Search for endComment and extract everything in between. + // TODO optimize -- there may be a better way of pulling out nodes. + + var endCommentText = "end-${_beginComment.text}"; + + var next; + for (next = _beginComment.nextNode; + next.nodeType != dom.Node.COMMENT_NODE && next.text != endCommentText; + next = _beginComment.nextNode) { + _childNodes.add(next); + next.remove(); + } + assert(next.nodeType == dom.Node.COMMENT_NODE && next.text == endCommentText); + next.remove(); + } +} + +class TranscludingComponentFactory implements ComponentFactory { + final Expando _expando; + + TranscludingComponentFactory(this._expando); + + FactoryFn call(dom.Node node, DirectiveRef ref) { + // CSS is not supported. + assert((ref.annotation as Component).cssUrls == null || + (ref.annotation as Component).cssUrls.isEmpty); + + var element = node as dom.Element; + return (Injector injector) { + var childInjector; + var component = ref.annotation as Component; + Scope scope = injector.get(Scope); + ViewCache viewCache = injector.get(ViewCache); + Http http = injector.get(Http); + TemplateCache templateCache = injector.get(TemplateCache); + DirectiveMap directives = injector.get(DirectiveMap); + NgBaseCss baseCss = injector.get(NgBaseCss); + + var contentPort = new _ContentPort(element); + + // Append the component's template as children + var viewFuture = ComponentFactory._viewFuture(component, viewCache, directives); + + if (viewFuture != null) { + viewFuture = viewFuture.then((ViewFactory viewFactory) { + contentPort.pullNodes(); + element.nodes.addAll(viewFactory(childInjector).nodes); + return element; + }); + } else { + viewFuture = new async.Future.microtask(() => contentPort.pullNodes()); + } + TemplateLoader templateLoader = new TemplateLoader(viewFuture); + + Scope shadowScope = scope.createChild({}); + + var probe; + var childModule = new Module() + ..type(ref.type) + ..type(NgElement) + ..value(_ContentPort, contentPort) + ..value(Scope, shadowScope) + ..value(TemplateLoader, templateLoader) + ..value(dom.ShadowRoot, new ShadowlessShadowRoot(element)) + ..factory(ElementProbe, (_) => probe); + childInjector = injector.createChild([childModule], name: SHADOW_DOM_INJECTOR_NAME); + + var controller = childInjector.get(ref.type); + shadowScope.context[component.publishAs] = controller; + ComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + return controller; + }; + } +} diff --git a/lib/directive/ng_template.dart b/lib/directive/ng_template.dart index 4d67ec8bd..969df2054 100644 --- a/lib/directive/ng_template.dart +++ b/lib/directive/ng_template.dart @@ -35,3 +35,5 @@ class NgTemplate { ? (element as dom.TemplateElement).content.innerHtml : element.innerHtml)); } + + diff --git a/pubspec.lock b/pubspec.lock index 22566a65d..d4a3aba82 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -12,7 +12,7 @@ packages: barback: description: barback source: hosted - version: "0.13.0" + version: "0.12.0" benchmark_harness: description: benchmark_harness source: hosted diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index c5f106bc3..f86eea9aa 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -19,6 +19,16 @@ forBothCompilers(fn) { }); fn(); }); + + describe('transcluding components', () { + beforeEachModule((Module m) { + m.type(Compiler, implementedBy: TaggingCompiler); + m.type(ComponentFactory, implementedBy: TranscludingComponentFactory); + + return m; + }); + fn(); + }); } void main() { @@ -207,7 +217,6 @@ void main() { describe('components', () { beforeEachModule((Module module) { module - ..type(SimpleComponent) ..type(CamelCaseMapComponent) ..type(IoComponent) ..type(IoControllerComponent) @@ -221,31 +230,77 @@ void main() { ..type(AttachDetachComponent) ..type(SimpleAttachComponent) ..type(SimpleComponent) + ..type(SometimesComponent) ..type(ExprAttrComponent) ..type(LogElementComponent) ..type(SayHelloFilter); }); - it('should select on element', async((VmTurnZone zone) { + it('should select on element', async(() { var element = _.compile(r'
'); microLeap(); _.rootScope.apply(); expect(element).toHaveText('INNER()'); })); + it('should tranclude correctly', async(() { + var element = _.compile(r'
trans
'); + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('INNER(trans)'); + })); + + it('should tranclude if content was not present initially', async(() { + var element = _.compile(r'
And jump
'); + document.body.append(element); + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('And '); + + _.rootScope.context['sometimes'] = true; + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('And jump'); + })); + + it('should redistribute content when the content tag disappears', async(() { + var element = _.compile(r'
And jump
'); + document.body.append(element); + + _.rootScope.context['sometimes'] = true; + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('And jump'); + + _.rootScope.context['sometimes'] = false; + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('And '); + + _.rootScope.context['sometimes'] = true; + microLeap(); + _.rootScope.apply(); + expect(element).toHaveText('And jump'); + })); + it('should store ElementProbe with Elements', async(() { _.compile('
innerText
'); microLeap(); + _.rootScope.apply(); var simpleElement = _.rootElement.querySelector('simple'); - expect(simpleElement.text).toEqual('innerText'); + expect(simpleElement).toHaveText('INNER(innerText)'); var simpleProbe = ngProbe(simpleElement); var simpleComponent = simpleProbe.injector.get(SimpleComponent); expect(simpleComponent.scope.context['name']).toEqual('INNER'); var shadowRoot = simpleElement.shadowRoot; - var shadowProbe = ngProbe(shadowRoot); - expect(shadowProbe).toBeNotNull(); - expect(shadowProbe.element).toEqual(shadowRoot); - expect(shadowProbe.parent.element).toEqual(simpleElement); + + // If there is no shadow root, skip this. + if (shadowRoot != null) { + var shadowProbe = ngProbe(shadowRoot); + expect(shadowProbe).toBeNotNull(); + expect(shadowProbe.element).toEqual(shadowRoot); + expect(shadowProbe.parent.element).toEqual(simpleElement); + } })); it('should create a simple component', async((VmTurnZone zone) { @@ -441,7 +496,7 @@ void main() { } })); - it('should publish component controller into the scope', async((VmTurnZone zone) { + it('should publish component controller into the scope', async(() { var element = _.compile(r'
'); microLeap(); _.rootScope.apply(); @@ -479,7 +534,7 @@ void main() { ..value(MockHttpBackend, httpBackend); }); - it('should fire onTemplate method', async((Compiler compile, Logger logger, MockHttpBackend backend) { + it('should fire onShadowRoot method', async((Compiler compile, Logger logger, MockHttpBackend backend) { backend.whenGET('some/template.url').respond(200, '
WORKED
'); var scope = _.rootScope.createChild({}); scope.context['isReady'] = 'ready'; @@ -532,7 +587,9 @@ void main() { backend.whenGET('foo.html').respond('
WORKED
'); _.compile(''); Element element = _.rootElement; - expect(log).toEqual([element, element, element.shadowRoot]); + expect(log).toEqual([element, element, + // If we don't have a shadowRoot, this is an invalid check + element.shadowRoot != null ? element.shadowRoot : log[2]]); })); }); @@ -561,6 +618,35 @@ void main() { }).toThrow('Unknown selector format \'buttonbar button\' for InvalidSelector'); }); }); + + describe('useShadowDom option', () { + beforeEachModule((Module m) { + m.type(ShadowyComponent); + m.type(ShadowlessComponent); + }); + + it('should create shadowy components', async((Logger log) { + _.compile(''); + expect(log).toEqual(['shadowy']); + expect(_.rootElement.shadowRoot).toBeNotNull(); + })); + + it('should create shadowless components', async((Logger log) { + _.compile(''); + expect(log).toEqual(['shadowless']); + expect(_.rootElement.shadowRoot).toBeNull(); + })); + + it('should create other components with the default strategy', async((ComponentFactory factory) { + _.compile(''); + if (factory is TranscludingComponentFactory) { + expect(_.rootElement.shadowRoot).toBeNull(); + } else { + expect(factory is ShadowDomComponentFactory).toBeTruthy(); + expect(_.rootElement.shadowRoot).toBeNotNull(); + } + })); + }); }); @@ -750,6 +836,37 @@ class SimpleComponent { } } +@Component( + selector: 'shadowy', + template: r'With shadow DOM', + useShadowDom: true +) +class ShadowyComponent { + ShadowyComponent(Logger log) { + log('shadowy'); + } +} + +@Component( + selector: 'shadowless', + template: r'Without shadow DOM', + useShadowDom: false +) +class ShadowlessComponent { + ShadowlessComponent(Logger log) { + log('shadowless'); + } +} + +@Component( + selector: 'sometimes', + template: r'
', + publishAs: 'ctrl') +class SometimesComponent { + @NgTwoWay('sometimes') + var sometimes; +} + @Component( selector: 'io', template: r'',