From fbcc59dc674e9676923aaf58ee47827e9c609d06 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 2 Jan 2015 14:23:59 -0800 Subject: [PATCH] feat(components): initial implementation of emulated content tag --- modules/core/src/compiler/compiler.js | 10 +- .../core/src/compiler/directive_metadata.js | 6 +- .../src/compiler/directive_metadata_reader.js | 23 +- modules/core/src/compiler/element_injector.js | 31 ++- .../shadow_dom_emulation/content_tag.js | 51 +++++ .../shadow_dom_emulation/light_dom.js | 81 +++++++ .../core/src/compiler/shadow_dom_strategy.js | 24 +- modules/core/src/compiler/view.js | 68 ++++-- modules/core/src/compiler/viewport.js | 28 ++- modules/core/src/dom/element.js | 7 +- .../directive_metadata_reader_spec.js | 52 ++++- .../test/compiler/element_injector_spec.js | 55 ++++- .../core/test/compiler/integration_spec.js | 45 +++- .../compiler/shadow_dom/content_tag_spec.js | 55 +++++ .../compiler/shadow_dom/light_dom_spec.js | 209 ++++++++++++++++++ modules/core/test/compiler/view_spec.js | 39 +++- modules/facade/src/collection.dart | 6 + modules/facade/src/collection.es6 | 9 + modules/facade/src/dom.dart | 24 ++ modules/facade/src/dom.es6 | 32 ++- 20 files changed, 798 insertions(+), 57 deletions(-) create mode 100644 modules/core/src/compiler/shadow_dom_emulation/content_tag.js create mode 100644 modules/core/src/compiler/shadow_dom_emulation/light_dom.js create mode 100644 modules/core/test/compiler/shadow_dom/content_tag_spec.js create mode 100644 modules/core/test/compiler/shadow_dom/light_dom_spec.js diff --git a/modules/core/src/compiler/compiler.js b/modules/core/src/compiler/compiler.js index 2f603e2040..64d5dd9299 100644 --- a/modules/core/src/compiler/compiler.js +++ b/modules/core/src/compiler/compiler.js @@ -13,6 +13,7 @@ import {createDefaultSteps} from './pipeline/default_steps'; import {TemplateLoader} from './template_loader'; import {DirectiveMetadata} from './directive_metadata'; import {Component} from '../annotations/annotations'; +import {Content} from './shadow_dom_emulation/content_tag'; /** * Cache that stores the ProtoView of the template of a component. @@ -60,13 +61,8 @@ export class Compiler { } createSteps(component:DirectiveMetadata):List { - var annotation: Component = component.annotation; - var directives = annotation.template.directives; - var annotatedDirectives = ListWrapper.create(); - for (var i=0; i this._reader.read(d)); + return createDefaultSteps(this._parser, component, dirs); } compile(component:Type, templateRoot:Element = null):Promise { diff --git a/modules/core/src/compiler/directive_metadata.js b/modules/core/src/compiler/directive_metadata.js index 743ac4464c..056f6c3eb0 100644 --- a/modules/core/src/compiler/directive_metadata.js +++ b/modules/core/src/compiler/directive_metadata.js @@ -1,5 +1,6 @@ import {Type, FIELD} from 'facade/lang'; import {Directive} from '../annotations/annotations' +import {List} from 'facade/collection' import {ShadowDomStrategy} from './shadow_dom'; /** @@ -9,10 +10,13 @@ export class DirectiveMetadata { type:Type; annotation:Directive; shadowDomStrategy:ShadowDomStrategy; + componentDirectives:List; - constructor(type:Type, annotation:Directive, shadowDomStrategy:ShadowDomStrategy) { + constructor(type:Type, annotation:Directive, shadowDomStrategy:ShadowDomStrategy, + componentDirectives:List) { this.annotation = annotation; this.type = type; this.shadowDomStrategy = shadowDomStrategy; + this.componentDirectives = componentDirectives; } } diff --git a/modules/core/src/compiler/directive_metadata_reader.js b/modules/core/src/compiler/directive_metadata_reader.js index 1e0629db55..0fb0bcc367 100644 --- a/modules/core/src/compiler/directive_metadata_reader.js +++ b/modules/core/src/compiler/directive_metadata_reader.js @@ -1,4 +1,5 @@ import {Type, isPresent, BaseException, stringify} from 'facade/lang'; +import {List, ListWrapper} from 'facade/collection'; import {Directive, Component} from '../annotations/annotations'; import {DirectiveMetadata} from './directive_metadata'; import {reflector} from 'reflection/reflection'; @@ -12,11 +13,17 @@ export class DirectiveMetadataReader { var annotation = annotations[i]; if (annotation instanceof Component) { - return new DirectiveMetadata(type, annotation, this.parseShadowDomStrategy(annotation)); + var shadowDomStrategy = this.parseShadowDomStrategy(annotation); + return new DirectiveMetadata( + type, + annotation, + shadowDomStrategy, + this.componentDirectivesMetadata(annotation, shadowDomStrategy) + ); } if (annotation instanceof Directive) { - return new DirectiveMetadata(type, annotation, null); + return new DirectiveMetadata(type, annotation, null, null); } } } @@ -26,4 +33,16 @@ export class DirectiveMetadataReader { parseShadowDomStrategy(annotation:Component):ShadowDomStrategy{ return isPresent(annotation.shadowDom) ? annotation.shadowDom : ShadowDomNative; } + + componentDirectivesMetadata(annotation:Component, shadowDomStrategy:ShadowDomStrategy):List { + var polyDirs = shadowDomStrategy.polyfillDirectives(); + var template = annotation.template; + var templateDirs = isPresent(template) && isPresent(template.directives) ? template.directives : []; + + var res = []; + res = ListWrapper.concat(res, templateDirs) + res = ListWrapper.concat(res, polyDirs) + + return res; + } } diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index 5dab9c8a67..dc883c8625 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -4,6 +4,7 @@ import {List, ListWrapper} from 'facade/collection'; import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'di/di'; import {Parent, Ancestor} from 'core/annotations/visibility'; import {View} from 'core/compiler/view'; +import {LightDom, SourceLightDom, DestinationLightDom} from 'core/compiler/shadow_dom_emulation/light_dom'; import {ViewPort} from 'core/compiler/viewport'; import {NgElement} from 'core/dom/element'; @@ -19,11 +20,16 @@ class StaticKeys { viewId:int; ngElementId:int; viewPortId:int; + destinationLightDomId:int; + sourceLightDomId:int; + constructor() { //TODO: vsavkin Key.annotate(Key.get(View), 'static') this.viewId = Key.get(View).id; this.ngElementId = Key.get(NgElement).id; this.viewPortId = Key.get(ViewPort).id; + this.destinationLightDomId = Key.get(DestinationLightDom).id; + this.sourceLightDomId = Key.get(SourceLightDom).id; } static instance() { @@ -105,10 +111,12 @@ export class PreBuiltObjects { view:View; element:NgElement; viewPort:ViewPort; - constructor(view, element:NgElement, viewPort: ViewPort) { + lightDom:LightDom; + constructor(view, element:NgElement, viewPort:ViewPort, lightDom:LightDom) { this.view = view; this.element = element; this.viewPort = viewPort; + this.lightDom = lightDom; } } @@ -306,6 +314,15 @@ export class ElementInjector extends TreeNode { return this._getByKey(Key.get(token), 0, null); } + hasDirective(type:Type):boolean { + return this._getDirectiveByKeyId(Key.get(type).id) !== _undefined; + } + + hasPreBuiltObject(type:Type):boolean { + var pb = this._getPreBuiltObjectByKeyId(Key.get(type).id); + return pb !== _undefined && isPresent(pb); + } + getComponent() { if (this._proto._binding0IsComponent) { return this._obj0; @@ -421,11 +438,15 @@ export class ElementInjector extends TreeNode { var staticKeys = StaticKeys.instance(); if (keyId === staticKeys.viewId) return this._preBuiltObjects.view; if (keyId === staticKeys.ngElementId) return this._preBuiltObjects.element; - if (keyId === staticKeys.viewPortId) { - if (isBlank(staticKeys.viewPortId)) throw new BaseException( - 'ViewPort is constructed only for @Template directives'); - return this._preBuiltObjects.viewPort; + if (keyId === staticKeys.viewPortId) return this._preBuiltObjects.viewPort; + if (keyId === staticKeys.destinationLightDomId) { + var p:ElementInjector = this._parent; + return isPresent(p) ? p._preBuiltObjects.lightDom : null; } + if (keyId === staticKeys.sourceLightDomId) { + return this._host._preBuiltObjects.lightDom; + } + //TODO add other objects as needed return _undefined; } diff --git a/modules/core/src/compiler/shadow_dom_emulation/content_tag.js b/modules/core/src/compiler/shadow_dom_emulation/content_tag.js new file mode 100644 index 0000000000..f79a63f236 --- /dev/null +++ b/modules/core/src/compiler/shadow_dom_emulation/content_tag.js @@ -0,0 +1,51 @@ +import {Decorator} from '../../annotations/annotations'; +import {SourceLightDom, DestinationLightDom, LightDom} from './light_dom'; +import {Inject} from 'di/di'; +import {Element, Node, DOM} from 'facade/dom'; +import {List, ListWrapper} from 'facade/collection'; +import {NgElement} from 'core/dom/element'; + +var _scriptTemplate = DOM.createScriptTag('type', 'ng/content') + +@Decorator({ + selector: 'content' +}) +export class Content { + _destinationLightDom:LightDom; + + _beginScript:Element; + _endScript:Element; + + select:string; + + constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) { + this._destinationLightDom = destinationLightDom; + + this.select = contentEl.getAttribute('select'); + + this._replaceContentElementWithScriptTags(contentEl.domElement); + } + + insert(nodes:List) { + DOM.insertAllBefore(this._endScript, nodes); + this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this._endScript : nodes[0]); + } + + _replaceContentElementWithScriptTags(contentEl:Element) { + this._beginScript = DOM.clone(_scriptTemplate); + this._endScript = DOM.clone(_scriptTemplate); + + DOM.insertBefore(contentEl, this._beginScript); + DOM.insertBefore(contentEl, this._endScript); + DOM.removeChild(DOM.parentElement(contentEl), contentEl); + } + + _removeNodesUntil(node:Node) { + var p = DOM.parentElement(this._beginScript); + for (var next = DOM.nextSibling(this._beginScript); + next !== node; + next = DOM.nextSibling(this._beginScript)) { + DOM.removeChild(p, next); + } + } +} \ No newline at end of file diff --git a/modules/core/src/compiler/shadow_dom_emulation/light_dom.js b/modules/core/src/compiler/shadow_dom_emulation/light_dom.js new file mode 100644 index 0000000000..2a3f7febd5 --- /dev/null +++ b/modules/core/src/compiler/shadow_dom_emulation/light_dom.js @@ -0,0 +1,81 @@ +import {Element, Node, DOM} from 'facade/dom'; +import {List, ListWrapper} from 'facade/collection'; +import {isBlank, isPresent} from 'facade/lang'; + +import {View} from '../view'; +import {ElementInjector} from '../element_injector'; +import {ViewPort} from '../viewport'; +import {Content} from './content_tag'; + +export class SourceLightDom {} +export class DestinationLightDom {} + +// TODO: LightDom should implement SourceLightDom and DestinationLightDom +// once interfaces are supported +export class LightDom { + lightDomView:View; + shadowDomView:View; + roots:List; + + constructor(lightDomView:View, shadowDomView:View, element:Element) { + this.lightDomView = lightDomView; + this.shadowDomView = shadowDomView; + this.roots = DOM.childNodesAsList(element); + DOM.clearNodes(element); + } + + redistribute() { + redistributeNodes(this.contentTags(), this.expandedDomNodes()); + } + + contentTags(): List { + return this._collectAllContentTags(this.shadowDomView, []); + } + + _collectAllContentTags(item, acc:List):List { + ListWrapper.forEach(item.elementInjectors, (ei) => { + if (ei.hasDirective(Content)) { + ListWrapper.push(acc, ei.get(Content)); + + } else if (ei.hasPreBuiltObject(ViewPort)) { + var vp = ei.get(ViewPort); + ListWrapper.forEach(vp.contentTagContainers(), (c) => { + this._collectAllContentTags(c, acc); + }); + } + }); + return acc; + } + + expandedDomNodes():List { + var res = []; + ListWrapper.forEach(this.roots, (root) => { + // TODO: vsavkin calculcate this info statically when creating light dom + var viewPort = this.lightDomView.getViewPortByTemplateElement(root); + if (isPresent(viewPort)) { + res = ListWrapper.concat(res, viewPort.nodes()); + } else { + ListWrapper.push(res, root); + } + }); + return res; + } +} + +function redistributeNodes(contents:List, nodes:List) { + for (var i = 0; i < contents.length; ++i) { + var content = contents[i]; + var select = content.select; + var matchSelector = (n) => DOM.elementMatches(n, select); + + if (isBlank(select)) { + content.insert(nodes); + ListWrapper.clear(nodes); + + } else { + var matchingNodes = ListWrapper.filter(nodes, matchSelector); + content.insert(matchingNodes); + ListWrapper.removeAll(nodes, matchingNodes); + } + } +} diff --git a/modules/core/src/compiler/shadow_dom_strategy.js b/modules/core/src/compiler/shadow_dom_strategy.js index 21b11093d0..38ca5a50c4 100644 --- a/modules/core/src/compiler/shadow_dom_strategy.js +++ b/modules/core/src/compiler/shadow_dom_strategy.js @@ -1,11 +1,15 @@ import {CONST} from 'facade/lang'; -import {DOM} from 'facade/dom'; -import {Element} from 'facade/dom'; +import {DOM, Element} from 'facade/dom'; +import {List} from 'facade/collection'; import {View} from './view'; +import {Content} from './shadow_dom_emulation/content_tag'; +import {LightDom} from './shadow_dom_emulation/light_dom'; export class ShadowDomStrategy { @CONST() constructor() {} attachTemplate(el:Element, view:View){} + constructLightDom(lightDomView:View, shadowDomView:View, el:Element){} + polyfillDirectives():List{ return null; }; } export class EmulatedShadowDomStrategy extends ShadowDomStrategy { @@ -14,6 +18,14 @@ export class EmulatedShadowDomStrategy extends ShadowDomStrategy { DOM.clearNodes(el); moveViewNodesIntoParent(el, view); } + + constructLightDom(lightDomView:View, shadowDomView:View, el:Element){ + return new LightDom(lightDomView, shadowDomView, el); + } + + polyfillDirectives():List { + return [Content]; + } } export class NativeShadowDomStrategy extends ShadowDomStrategy { @@ -21,6 +33,14 @@ export class NativeShadowDomStrategy extends ShadowDomStrategy { attachTemplate(el:Element, view:View){ moveViewNodesIntoParent(el.createShadowRoot(), view); } + + constructLightDom(lightDomView:View, shadowDomView:View, el:Element){ + return null; + } + + polyfillDirectives():List { + return []; + } } function moveViewNodesIntoParent(parent, view) { diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index c479f269fd..c566b75560 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -12,6 +12,8 @@ import {Injector} from 'di/di'; import {NgElement} from 'core/dom/element'; import {ViewPort} from './viewport'; import {OnChange} from './interfaces'; +import {Content} from './shadow_dom_emulation/content_tag'; +import {LightDom, DestinationLightDom} from './shadow_dom_emulation/light_dom'; const NG_BINDING_CLASS = 'ng-binding'; const NG_BINDING_CLASS_SELECTOR = '.ng-binding'; @@ -38,6 +40,7 @@ export class View { proto: ProtoView; context: any; contextWithLocals:ContextWithVariableBindings; + constructor(proto:ProtoView, nodes:List, protoRecordRange:ProtoRecordRange, protoContextLocals:Map) { this.proto = proto; this.nodes = nodes; @@ -156,7 +159,12 @@ export class View { // componentChildViews if (isPresent(shadowDomAppInjector)) { this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector, - elementInjector, elementInjector.getComponent()); + elementInjector, elementInjector.getComponent()); + } + + if (isPresent(componentDirective)) { + var lightDom = this.preBuiltObjects[i].lightDom; + if (isPresent(lightDom)) lightDom.redistribute(); } } } @@ -191,6 +199,16 @@ export class View { } } + getViewPortByTemplateElement(node):ViewPort { + if (!(node instanceof Element)) return null; + + for (var i = 0; i < this.viewPorts.length; ++i) { + if (this.viewPorts[i].templateElement === node) return this.viewPorts[i]; + } + + return null; + } + _invokeMementoForRecords(records:List) { for(var i = 0; i < records.length; ++i) { this._invokeMementoFor(records[i]); @@ -267,12 +285,18 @@ export class ProtoView { // TODO(rado): hostElementInjector should be moved to hydrate phase. instantiate(hostElementInjector: ElementInjector):View { var rootElementClone = this.instantiateInPlace ? this.element : DOM.clone(this.element); - var elementsWithBindings; + var elementsWithBindingsDynamic; if (this.isTemplateElement) { - elementsWithBindings = DOM.querySelectorAll(rootElementClone.content, NG_BINDING_CLASS_SELECTOR); + elementsWithBindingsDynamic = DOM.querySelectorAll(rootElementClone.content, NG_BINDING_CLASS_SELECTOR); } else { - elementsWithBindings = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS); + elementsWithBindingsDynamic= DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS); } + + var elementsWithBindings = ListWrapper.createFixedSize(elementsWithBindingsDynamic.length); + for (var i = 0; i < elementsWithBindingsDynamic.length; ++i) { + elementsWithBindings[i] = elementsWithBindingsDynamic[i]; + } + var viewNodes; if (this.isTemplateElement) { var childNode = DOM.firstChild(rootElementClone.content); @@ -319,21 +343,6 @@ export class ProtoView { } elementInjectors[i] = elementInjector; - // viewPorts - var viewPort = null; - if (isPresent(binder.templateDirective)) { - viewPort = new ViewPort(view, element, binder.nestedProtoView, elementInjector); - ListWrapper.push(viewPorts, viewPort); - } - - // preBuiltObjects - var preBuiltObject = null; - if (isPresent(elementInjector)) { - preBuiltObject = new PreBuiltObjects(view, new NgElement(element), viewPort); - } - preBuiltObjects[i] = preBuiltObject; - - // elementsWithPropertyBindings if (binder.hasElementPropertyBindings) { ListWrapper.push(elementsWithPropertyBindings, element); } @@ -351,13 +360,29 @@ export class ProtoView { } // componentChildViews + var lightDom = null; if (isPresent(binder.componentDirective)) { var childView = binder.nestedProtoView.instantiate(elementInjector); view.recordRange.addRange(childView.recordRange); + lightDom = binder.componentDirective.shadowDomStrategy.constructLightDom(view, childView, element); binder.componentDirective.shadowDomStrategy.attachTemplate(element, childView); + ListWrapper.push(componentChildViews, childView); } + + // viewPorts + var viewPort = null; + if (isPresent(binder.templateDirective)) { + var destLightDom = this._parentElementLightDom(protoElementInjector, preBuiltObjects); + viewPort = new ViewPort(view, element, binder.nestedProtoView, elementInjector, destLightDom); + ListWrapper.push(viewPorts, viewPort); + } + + // preBuiltObjects + if (isPresent(elementInjector)) { + preBuiltObjects[i] = new PreBuiltObjects(view, new NgElement(element), viewPort, lightDom); + } } view.init(elementInjectors, rootElementInjectors, textNodes, elementsWithPropertyBindings, @@ -366,6 +391,11 @@ export class ProtoView { return view; } + _parentElementLightDom(protoElementInjector:ProtoElementInjector, preBuiltObjects:List):LightDom { + var p = protoElementInjector.parent; + return isPresent(p) ? preBuiltObjects[p.index].lightDom : null; + } + bindVariable(contextName:string, templateName:string) { MapWrapper.set(this.variableBindings, contextName, templateName); MapWrapper.set(this.protoContextLocals, templateName, null); diff --git a/modules/core/src/compiler/viewport.js b/modules/core/src/compiler/viewport.js index f9d613e346..ade1c237cf 100644 --- a/modules/core/src/compiler/viewport.js +++ b/modules/core/src/compiler/viewport.js @@ -12,16 +12,18 @@ export class ViewPort { defaultProtoView: ProtoView; _views: List; _viewLastNode: List; + _lightDom: any; elementInjector: ElementInjector; appInjector: Injector; hostElementInjector: ElementInjector; constructor(parentView: View, templateElement: Element, defaultProtoView: ProtoView, - elementInjector: ElementInjector) { + elementInjector: ElementInjector, lightDom = null) { this.parentView = parentView; this.templateElement = templateElement; this.defaultProtoView = defaultProtoView; this.elementInjector = elementInjector; + this._lightDom = lightDom; // The order in this list matches the DOM order. this._views = []; @@ -77,7 +79,11 @@ export class ViewPort { insert(view, atIndex=-1): View { if (atIndex == -1) atIndex = this._views.length; ListWrapper.insert(this._views, atIndex, view); - ViewPort.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view); + if (isBlank(this._lightDom)) { + ViewPort.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view); + } else { + this._lightDom.redistribute(); + } this.parentView.recordRange.addRange(view.recordRange); this._linkElementInjectors(view); return view; @@ -87,12 +93,28 @@ export class ViewPort { if (atIndex == -1) atIndex = this._views.length - 1; var removedView = this.get(atIndex); ListWrapper.removeAt(this._views, atIndex); - ViewPort.removeViewNodesFromParent(this.templateElement.parentNode, removedView); + if (isBlank(this._lightDom)) { + ViewPort.removeViewNodesFromParent(this.templateElement.parentNode, removedView); + } else { + this._lightDom.redistribute(); + } removedView.recordRange.remove(); this._unlinkElementInjectors(removedView); return removedView; } + contentTagContainers() { + return this._views; + } + + nodes():List { + var r = []; + for (var i = 0; i < this._views.length; ++i) { + r = ListWrapper.concat(r, this._views[i].nodes); + } + return r; + } + _linkElementInjectors(view) { for (var i = 0; i < view.rootElementInjectors.length; ++i) { view.rootElementInjectors[i].parent = this.elementInjector; diff --git a/modules/core/src/dom/element.js b/modules/core/src/dom/element.js index fccf7857bb..056b184c63 100644 --- a/modules/core/src/dom/element.js +++ b/modules/core/src/dom/element.js @@ -1,8 +1,13 @@ -import {Element} from 'facade/dom'; +import {DOM, Element} from 'facade/dom'; +import {normalizeBlank} from 'facade/lang'; export class NgElement { domElement:Element; constructor(domElement:Element) { this.domElement = domElement; } + + getAttribute(name:string) { + return normalizeBlank(DOM.getAttribute(this.domElement, name)); + } } \ No newline at end of file diff --git a/modules/core/test/compiler/directive_metadata_reader_spec.js b/modules/core/test/compiler/directive_metadata_reader_spec.js index 45011f9383..56e71c4734 100644 --- a/modules/core/test/compiler/directive_metadata_reader_spec.js +++ b/modules/core/test/compiler/directive_metadata_reader_spec.js @@ -1,8 +1,20 @@ import {ddescribe, describe, it, iit, expect, beforeEach} from 'test_lib/test_lib'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; import {Decorator, Component} from 'core/annotations/annotations'; +import {TemplateConfig} from 'core/annotations/template_config'; import {DirectiveMetadata} from 'core/compiler/directive_metadata'; -import {ShadowDomEmulated, ShadowDomNative} from 'core/compiler/shadow_dom'; +import {ShadowDomStrategy, ShadowDomNative} from 'core/compiler/shadow_dom'; +import {CONST} from 'facade/lang'; + + +class FakeShadowDomStrategy extends ShadowDomStrategy { + @CONST() + constructor() {} + + polyfillDirectives() { + return [SomeDirective]; + } +} @Decorator({ selector: 'someSelector' @@ -17,13 +29,28 @@ class ComponentWithoutExplicitShadowDomStrategy {} @Component({ selector: 'someSelector', - shadowDom: ShadowDomEmulated + shadowDom: new FakeShadowDomStrategy() }) class ComponentWithExplicitShadowDomStrategy {} class SomeDirectiveWithoutAnnotation { } +@Component({ + selector: 'withoutDirectives' +}) +class ComponentWithoutDirectives {} + +@Component({ + selector: 'withDirectives', + template: new TemplateConfig({ + directives: [ComponentWithoutDirectives] + }) +}) +class ComponentWithDirectives {} + + + export function main() { describe("DirectiveMetadataReader", () => { var reader; @@ -35,7 +62,7 @@ export function main() { it('should read out the annotation', () => { var directiveMetadata = reader.read(SomeDirective); expect(directiveMetadata).toEqual( - new DirectiveMetadata(SomeDirective, new Decorator({selector: 'someSelector'}), null)); + new DirectiveMetadata(SomeDirective, new Decorator({selector: 'someSelector'}), null, null)); }); it('should throw if not matching annotation is found', () => { @@ -47,7 +74,7 @@ export function main() { describe("shadow dom strategy", () => { it('should return the provided shadow dom strategy when it is present', () => { var directiveMetadata = reader.read(ComponentWithExplicitShadowDomStrategy); - expect(directiveMetadata.shadowDomStrategy).toEqual(ShadowDomEmulated); + expect(directiveMetadata.shadowDomStrategy).toBeAnInstanceOf(FakeShadowDomStrategy); }); it('should return Native otherwise', () => { @@ -55,5 +82,22 @@ export function main() { expect(directiveMetadata.shadowDomStrategy).toEqual(ShadowDomNative); }); }); + + describe("componentDirectives", () => { + it("should return an empty list when no directives specified", () => { + var cmp = reader.read(ComponentWithoutDirectives); + expect(cmp.componentDirectives).toEqual([]); + }); + + it("should return a list of directives specified in the template config", () => { + var cmp = reader.read(ComponentWithDirectives); + expect(cmp.componentDirectives).toEqual([ComponentWithoutDirectives]); + }); + + it("should include directives required by the shadow DOM strategy", () => { + var cmp = reader.read(ComponentWithExplicitShadowDomStrategy); + expect(cmp.componentDirectives).toEqual([SomeDirective]); + }); + }); }); } \ No newline at end of file diff --git a/modules/core/test/compiler/element_injector_spec.js b/modules/core/test/compiler/element_injector_spec.js index e000d85215..cfa8551482 100644 --- a/modules/core/test/compiler/element_injector_spec.js +++ b/modules/core/test/compiler/element_injector_spec.js @@ -8,11 +8,16 @@ import {View} from 'core/compiler/view'; import {ProtoRecordRange} from 'change_detection/change_detection'; import {ViewPort} from 'core/compiler/viewport'; import {NgElement} from 'core/dom/element'; +import {LightDom, SourceLightDom, DestinationLightDom} from 'core/compiler/shadow_dom_emulation/light_dom'; @proxy @IMPLEMENTS(View) class DummyView extends SpyObject {noSuchMethod(m){super.noSuchMethod(m)}} +@proxy +@IMPLEMENTS(LightDom) +class DummyLightDom extends SpyObject {noSuchMethod(m){super.noSuchMethod(m)}} + class Directive { } @@ -65,7 +70,7 @@ class NeedsView { } export function main() { - var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null); + var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null, null); function humanize(tree, names:List) { var lookupName = (item) => @@ -88,12 +93,15 @@ export function main() { return inj; } - function parentChildInjectors(parentBindings, childBindings) { + function parentChildInjectors(parentBindings, childBindings, parentPreBuildObjects = null) { + if (isBlank(parentPreBuildObjects)) parentPreBuildObjects = defaultPreBuiltObjects; + var inj = new Injector([]); var protoParent = new ProtoElementInjector(null, 0, parentBindings); var parent = protoParent.instantiate(null, null); - parent.instantiateDirectives(inj, null, defaultPreBuiltObjects); + + parent.instantiateDirectives(inj, null, parentPreBuildObjects); var protoChild = new ProtoElementInjector(protoParent, 1, childBindings); var child = protoChild.instantiate(parent, null); @@ -102,13 +110,15 @@ export function main() { return child; } - function hostShadowInjectors(hostBindings, shadowBindings) { + function hostShadowInjectors(hostBindings, shadowBindings, hostPreBuildObjects = null) { + if (isBlank(hostPreBuildObjects)) hostPreBuildObjects = defaultPreBuiltObjects; + var inj = new Injector([]); var shadowInj = inj.createChild([]); var protoParent = new ProtoElementInjector(null, 0, hostBindings, true); var host = protoParent.instantiate(null, null); - host.instantiateDirectives(inj, shadowInj, null); + host.instantiateDirectives(inj, shadowInj, hostPreBuildObjects); var protoChild = new ProtoElementInjector(protoParent, 0, shadowBindings, false); var shadow = protoChild.instantiate(null, host); @@ -186,7 +196,7 @@ export function main() { it("should instantiate directives that depend on pre built objects", function () { var view = new DummyView(); - var inj = injector([NeedsView], null, null, new PreBuiltObjects(view, null, null)); + var inj = injector([NeedsView], null, null, new PreBuiltObjects(view, null, null, null)); expect(inj.get(NeedsView).view).toBe(view); }); @@ -291,24 +301,51 @@ export function main() { describe("pre built objects", function () { it("should return view", function () { var view = new DummyView(); - var inj = injector([], null, null, new PreBuiltObjects(view, null, null)); + var inj = injector([], null, null, new PreBuiltObjects(view, null, null, null)); expect(inj.get(View)).toEqual(view); }); it("should return element", function () { var element = new NgElement(null); - var inj = injector([], null, null, new PreBuiltObjects(null, element, null)); + var inj = injector([], null, null, new PreBuiltObjects(null, element, null, null)); expect(inj.get(NgElement)).toEqual(element); }); it('should return viewPort', function () { var viewPort = new ViewPort(null, null, null, null); - var inj = injector([], null, null, new PreBuiltObjects(null, null, viewPort)); + var inj = injector([], null, null, new PreBuiltObjects(null, null, viewPort, null)); expect(inj.get(ViewPort)).toEqual(viewPort); }); + + describe("light DOM", () => { + var lightDom, parentPreBuiltObjects; + + beforeEach(() => { + lightDom = new DummyLightDom(); + parentPreBuiltObjects = new PreBuiltObjects(null, null, null, lightDom); + }); + + it("should return destination light DOM from the parent's injector", function () { + var child = parentChildInjectors([], [], parentPreBuiltObjects); + + expect(child.get(DestinationLightDom)).toEqual(lightDom); + }); + + it("should return null when parent's injector is a component boundary", function () { + var child = hostShadowInjectors([], [], parentPreBuiltObjects); + + expect(child.get(DestinationLightDom)).toBeNull(); + }); + + it("should return source light DOM from the closest component boundary", function () { + var child = hostShadowInjectors([], [], parentPreBuiltObjects); + + expect(child.get(SourceLightDom)).toEqual(lightDom); + }); + }); }); }); } diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js index de63c7cc6b..74cbc2b023 100644 --- a/modules/core/test/compiler/integration_spec.js +++ b/modules/core/test/compiler/integration_spec.js @@ -7,6 +7,7 @@ import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; +import {ShadowDomEmulated} from 'core/compiler/shadow_dom'; import {Decorator, Component, Template} from 'core/annotations/annotations'; import {TemplateConfig} from 'core/annotations/template_config'; @@ -108,9 +109,51 @@ export function main() { }); }); }); + + it('should emulate content tag', (done) => { + var el = `` + + `
Light
` + + `
DOM
` + + `
`; + + function createView(pv) { + var view = pv.instantiate(null); + view.hydrate(new Injector([]), null, {}); + return view; + } + + compiler.compile(MyComp, createElement(el)). + then(createView). + then((view) => { + expect(DOM.getText(view.nodes[0])).toEqual('Before LightDOM After'); + done(); + }); + + }); }); } +@Template({ + selector: '[trivial-template]' +}) +class TrivialTemplateDirective { + constructor(viewPort:ViewPort) { + viewPort.create(); + } +} + +@Component({ + selector: 'emulated-shadow-dom-component', + template: new TemplateConfig({ + inline: 'Before After', + directives: [] + }), + shadowDom: ShadowDomEmulated +}) +class EmulatedShadowDomCmp { + +} + @Decorator({ selector: '[my-dir]', bind: {'elprop':'dirProp'} @@ -124,7 +167,7 @@ class MyDir { @Component({ template: new TemplateConfig({ - directives: [MyDir, ChildComp, SomeTemplate] + directives: [MyDir, ChildComp, SomeTemplate, EmulatedShadowDomCmp, TrivialTemplateDirective] }) }) class MyComp { diff --git a/modules/core/test/compiler/shadow_dom/content_tag_spec.js b/modules/core/test/compiler/shadow_dom/content_tag_spec.js new file mode 100644 index 0000000000..571baf1c22 --- /dev/null +++ b/modules/core/test/compiler/shadow_dom/content_tag_spec.js @@ -0,0 +1,55 @@ +import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject} from 'test_lib/test_lib'; +import {proxy, IMPLEMENTS} from 'facade/lang'; +import {DOM} from 'facade/dom'; +import {Content} from 'core/compiler/shadow_dom_emulation/content_tag'; +import {NgElement} from 'core/dom/element'; +import {LightDom} from 'core/compiler/shadow_dom_emulation/light_dom'; + +@proxy +@IMPLEMENTS(LightDom) +class DummyLightDom extends SpyObject {noSuchMethod(m){super.noSuchMethod(m)}} + +var _script = ``; + +export function main() { + describe('Content', function() { + it("should insert the nodes", () => { + var lightDom = new DummyLightDom(); + var parent = createElement("
"); + var content = DOM.firstChild(parent); + + var c = new Content(lightDom, new NgElement(content)); + c.insert([createElement(""), createElement("")]) + + expect(DOM.getInnerHTML(parent)).toEqual(`${_script}${_script}`); + }); + + it("should remove the nodes from the previous insertion", () => { + var lightDom = new DummyLightDom(); + var parent = createElement("
"); + var content = DOM.firstChild(parent); + + var c = new Content(lightDom, new NgElement(content)); + c.insert([createElement("")]); + c.insert([createElement("")]); + + expect(DOM.getInnerHTML(parent)).toEqual(`${_script}${_script}`); + }); + + it("should insert empty list", () => { + var lightDom = new DummyLightDom(); + var parent = createElement("
"); + var content = DOM.firstChild(parent); + + var c = new Content(lightDom, new NgElement(content)); + c.insert([createElement("")]); + c.insert([]); + + expect(DOM.getInnerHTML(parent)).toEqual(`${_script}${_script}`); + }); + }); +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/shadow_dom/light_dom_spec.js b/modules/core/test/compiler/shadow_dom/light_dom_spec.js new file mode 100644 index 0000000000..0f40f8ee86 --- /dev/null +++ b/modules/core/test/compiler/shadow_dom/light_dom_spec.js @@ -0,0 +1,209 @@ +import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject} from 'test_lib/test_lib'; +import {proxy, IMPLEMENTS, isBlank} from 'facade/lang'; +import {ListWrapper, MapWrapper} from 'facade/collection'; +import {DOM} from 'facade/dom'; +import {Content} from 'core/compiler/shadow_dom_emulation/content_tag'; +import {NgElement} from 'core/dom/element'; +import {LightDom} from 'core/compiler/shadow_dom_emulation/light_dom'; +import {View} from 'core/compiler/view'; +import {ViewPort} from 'core/compiler/viewport'; +import {ElementInjector} from 'core/compiler/element_injector'; +import {ProtoRecordRange} from 'change_detection/change_detection'; + +@proxy +@IMPLEMENTS(ElementInjector) +class FakeElementInjector { + content; + viewPort; + + constructor(content, viewPort) { + this.content = content; + this.viewPort = viewPort; + } + + hasDirective(type) { + return this.content != null; + } + + hasPreBuiltObject(type) { + return this.viewPort != null; + } + + get(t) { + if (t === Content) return this.content; + if (t === ViewPort) return this.viewPort; + return null; + } + + noSuchMethod(i) { + super.noSuchMethod(i); + } +} + +@proxy +@IMPLEMENTS(View) +class FakeView { + elementInjectors; + ports; + + constructor(elementInjectors = null, ports = null) { + this.elementInjectors = elementInjectors; + this.ports = ports; + } + + getViewPortByTemplateElement(el) { + if (isBlank(this.ports)) return null; + return MapWrapper.get(this.ports, el); + } + + noSuchMethod(i) { + super.noSuchMethod(i); + } +} + +@proxy +@IMPLEMENTS(ViewPort) +class FakeViewPort { + _nodes; + _contentTagContainers; + + constructor(nodes, views) { + this._nodes = nodes; + this._contentTagContainers = views; + } + + nodes(){ + return this._nodes; + } + + contentTagContainers(){ + return this._contentTagContainers; + } + + noSuchMethod(i) { + super.noSuchMethod(i); + } +} + + +@proxy +@IMPLEMENTS(Content) +class FakeContentTag { + select; + nodes; + + constructor(select = null) { + this.select = select; + } + + insert(nodes){ + this.nodes = ListWrapper.clone(nodes); + } + + noSuchMethod(i) { + super.noSuchMethod(i); + } +} + + +export function main() { + describe('LightDom', function() { + var lightDomView; + + beforeEach(() => { + lightDomView = new FakeView([], MapWrapper.create()); + }); + + describe("contentTags", () => { + it("should collect content tags from element injectors", () => { + var tag = new FakeContentTag(); + var shadowDomView = new FakeView([new FakeElementInjector(tag, null)]); + + var lightDom = new LightDom(lightDomView, shadowDomView, createElement("
")); + + expect(lightDom.contentTags()).toEqual([tag]); + }); + + it("should collect content tags from view ports", () => { + var tag = new FakeContentTag(); + var vp = new FakeViewPort(null, [ + new FakeView([new FakeElementInjector(tag, null)]) + ]); + + var shadowDomView = new FakeView([new FakeElementInjector(null, vp)]); + + var lightDom = new LightDom(lightDomView, shadowDomView, createElement("
")); + + expect(lightDom.contentTags()).toEqual([tag]); + }); + }); + + describe("expanded roots", () => { + it("should contain root nodes", () => { + var lightDomEl = createElement("
") + var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl); + expect(toHtml(lightDom.expandedDomNodes())).toEqual([""]); + }); + + it("should include view port nodes", () => { + var lightDomEl = createElement("
") + var template = lightDomEl.childNodes[0]; + + var lightDomView = new FakeView([], + MapWrapper.createFromPairs([ + [template, new FakeViewPort([createElement("")], null)] + ]) + ); + + var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl); + + expect(toHtml(lightDom.expandedDomNodes())).toEqual([""]); + }); + }); + + describe("redistribute", () => { + it("should redistribute nodes between content tags with select property set", () => { + var contentA = new FakeContentTag("a"); + var contentB = new FakeContentTag("b"); + + var lightDomEl = createElement("") + + var lightDom = new LightDom(lightDomView, new FakeView([ + new FakeElementInjector(contentA, null), + new FakeElementInjector(contentB, null) + ]), lightDomEl); + + lightDom.redistribute(); + + expect(toHtml(contentA.nodes)).toEqual(["1", "3"]); + expect(toHtml(contentB.nodes)).toEqual(["2"]); + }); + + it("should support wildcard content tags", () => { + var wildcard = new FakeContentTag(null); + var contentB = new FakeContentTag("b"); + + var lightDomEl = createElement("") + + var lightDom = new LightDom(lightDomView, new FakeView([ + new FakeElementInjector(wildcard, null), + new FakeElementInjector(contentB, null) + ]), lightDomEl); + + lightDom.redistribute(); + + expect(toHtml(wildcard.nodes)).toEqual(["1", "2", "3"]); + expect(toHtml(contentB.nodes)).toEqual([]); + }); + }); + }); +} + +function toHtml(nodes) { + if (isBlank(nodes)) return []; + return ListWrapper.map(nodes, DOM.getOuterHTML); +} + +function createElement(html) { + return DOM.createTemplate(html).content.firstChild; +} diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index 257763647d..d27e3727aa 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -7,14 +7,30 @@ import {Component, Decorator, Template} from 'core/annotations/annotations'; import {OnChange} from 'core/core'; import {Lexer, Parser, ProtoRecordRange, ChangeDetector} from 'change_detection/change_detection'; import {TemplateConfig} from 'core/annotations/template_config'; -import {List} from 'facade/collection'; +import {List, MapWrapper} from 'facade/collection'; import {DOM, Element} from 'facade/dom'; -import {int} from 'facade/lang'; +import {int, proxy, IMPLEMENTS} from 'facade/lang'; import {Injector} from 'di/di'; import {View} from 'core/compiler/view'; import {ViewPort} from 'core/compiler/viewport'; import {reflector} from 'reflection/reflection'; + +@proxy +@IMPLEMENTS(ViewPort) +class FakeViewPort { + templateElement; + + constructor(templateElement) { + this.templateElement = templateElement; + } + + noSuchMethod(i) { + super.noSuchMethod(i); + } +} + + export function main() { describe('view', function() { var parser, someComponentDirective, someTemplateDirective; @@ -53,6 +69,25 @@ export function main() { }); }); + describe("getViewPortByTemplateElement", () => { + var view, viewPort, templateElement; + + beforeEach(() => { + templateElement = createElement(""); + view = new View(null, null, new ProtoRecordRange(), MapWrapper.create()); + viewPort = new FakeViewPort(templateElement); + view.viewPorts = [viewPort]; + }); + + it("should return null when the given element is not an element", () => { + expect(view.getViewPortByTemplateElement("not an element")).toBeNull(); + }); + + it("should return a view port with the matching template element", () => { + expect(view.getViewPortByTemplateElement(templateElement)).toBe(viewPort); + }); + }); + describe('with locals', function() { var view; beforeEach(() => { diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index f5d76e0452..6d6e5db625 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -96,8 +96,14 @@ class ListWrapper { static bool isList(l) => l is List; static void insert(List l, int index, value) { l.insert(index, value); } static void removeAt(List l, int index) { l.removeAt(index); } + static void removeAll(List list, List items) { + for (var i = 0; i < items.length; ++i) { + list.remove(items[i]); + } + } static void clear(List l) { l.clear(); } static String join(List l, String s) => l.join(s); + static bool isEmpty(list) => list.isEmpty; } bool isListLikeIterable(obj) => obj is Iterable; diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index d8a55d96f9..5071a55df9 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -143,12 +143,21 @@ export class ListWrapper { list.splice(index, 1); return res; } + static removeAll(list, items) { + for (var i = 0; i < items.length; ++i) { + var index = list.indexOf(items[i]); + list.splice(index, 1); + } + } static clear(list) { list.splice(0, list.length); } static join(list, s) { return list.join(s); } + static isEmpty(list) { + return list.length == 0; + } } export function isListLikeIterable(obj):boolean { diff --git a/modules/facade/src/dom.dart b/modules/facade/src/dom.dart index e463a0679a..5e2b78073c 100644 --- a/modules/facade/src/dom.dart +++ b/modules/facade/src/dom.dart @@ -47,6 +47,9 @@ class DOM { static List childNodes(el) { return el.childNodes; } + static childNodesAsList(el) { + return childNodes(el).toList(); + } static clearNodes(el) { el.nodes = []; } @@ -56,6 +59,12 @@ class DOM { static removeChild(el, node) { node.remove(); } + static insertBefore(el, node) { + el.parentNode.insertBefore(node, el); + } + static insertAllBefore(el, nodes) { + el.parentNode.insertAllBefore(nodes, el); + } static insertAfter(el, node) { el.parentNode.insertBefore(node, el.nextNode); } @@ -74,6 +83,12 @@ class DOM { if (doc == null) doc = document; return doc.createElement(tagName); } + static createScriptTag(String attrName, String attrValue, [doc=null]) { + if (doc == null) doc = document; + var el = doc.createElement("SCRIPT"); + el.setAttribute(attrName, attrValue); + return el; + } static clone(Node node) { return node.clone(true); } @@ -95,9 +110,15 @@ class DOM { static hasClass(Element element, classname) { return element.classes.contains(classname); } + static String tagName(Element element) { + return element.tagName; + } static attributeMap(Element element) { return element.attributes; } + static getAttribute(Element element, String attribute) { + return element.getAttribute(attribute); + } static Node templateAwareRoot(Element el) { return el is TemplateElement ? el.content : el; } @@ -107,4 +128,7 @@ class DOM { static HtmlDocument defaultDoc() { return document; } + static bool elementMatches(n, String selector) { + return n is Element && n.matches(selector); + } } diff --git a/modules/facade/src/dom.es6 b/modules/facade/src/dom.es6 index b6560a0195..cfa0c2f36b 100644 --- a/modules/facade/src/dom.es6 +++ b/modules/facade/src/dom.es6 @@ -7,7 +7,7 @@ export var TemplateElement = window.HTMLTemplateElement; export var document = window.document; export var location = window.location; -import {List, MapWrapper} from 'facade/collection'; +import {List, MapWrapper, ListWrapper} from 'facade/collection'; export class DOM { static query(selector) { @@ -40,6 +40,14 @@ export class DOM { static childNodes(el):NodeList { return el.childNodes; } + static childNodesAsList(el):List { + var childNodes = el.childNodes; + var res = ListWrapper.createFixedSize(childNodes.length); + for (var i=0; i { + el.parentNode.insertBefore(n, el); + }); + } static insertAfter(el, node) { el.parentNode.insertBefore(node, el.nextSibling); } @@ -69,6 +85,11 @@ export class DOM { static createElement(tagName, doc=document) { return doc.createElement(tagName); } + static createScriptTag(attrName:string, attrValue:string, doc=document) { + var el = doc.createElement("SCRIPT"); + el.setAttribute(attrName, attrValue); + return el; + } static clone(node:Node) { return node.cloneNode(true); } @@ -90,6 +111,9 @@ export class DOM { static hasClass(element:Element, classname:string) { return element.classList.contains(classname); } + static tagName(element:Element):string { + return element.tagName; + } static attributeMap(element:Element) { var res = MapWrapper.create(); var elAttrs = element.attributes; @@ -99,6 +123,9 @@ export class DOM { } return res; } + static getAttribute(element:Element, attribute:string) { + return element.getAttribute(attribute); + } static templateAwareRoot(el:Element):Node { return el instanceof TemplateElement ? el.content : el; } @@ -108,4 +135,7 @@ export class DOM { static defaultDoc() { return document; } + static elementMatches(n, selector:string):boolean { + return n instanceof Element && n.matches(selector); + } }