diff --git a/lib/core_dom/module_internal.dart b/lib/core_dom/module_internal.dart index 6dd5aa5aa..a94445170 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,11 @@ class CoreDomModule extends Module { type(AttrMustache); type(Compiler, implementedBy: TaggingCompiler); + type(ComponentFactory, implementedBy: ShadowDomComponentFactory); + 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..8cfe4132e 100644 --- a/lib/core_dom/shadow_dom_component_factory.dart +++ b/lib/core_dom/shadow_dom_component_factory.dart @@ -2,6 +2,26 @@ part of angular.core.dom_internal; abstract class ComponentFactory { FactoryFn call(dom.Node node, DirectiveRef ref); + + static async.Future _viewFuture( + Component component, ViewCache viewCache, DirectiveMap directives) { + if (component.template != null) { + return new async.Future.value(viewCache.fromHtml(component.template, directives)); + } + 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 +32,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 +97,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 +111,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..baba003f8 --- /dev/null +++ b/lib/core_dom/transcluding_component_factory.dart @@ -0,0 +1,121 @@ +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); + + void attach() { + if (_port == null) return; + _beginComment = _port.content(_element); + } + + void detach() { + if (_port == null) return; + _port.detachContent(_beginComment); + } +} + +class ContentPort { + dom.Element _element; + var _childNodes = []; + + ContentPort(this._element); + + void pullNodes() { + _childNodes.addAll(_element.nodes); + _element.nodes = []; + } + + content(dom.Element elt) { + var hash = elt.hashCode; + var beginComment = new dom.Comment("content $hash"); + + if (_childNodes.isNotEmpty) { + 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; + } + + void detachContent(dom.Comment _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..5fb4002ac 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]]); })); }); @@ -750,6 +807,16 @@ class SimpleComponent { } } + +@Component( + selector: 'sometimes', + template: r'
', + publishAs: 'ctrl') +class SometimesComponent { + @NgTwoWay('sometimes') + var sometimes; +} + @Component( selector: 'io', template: r'',