feat(components): initial implementation of emulated content tag

This commit is contained in:
vsavkin 2015-01-02 14:23:59 -08:00
parent 0f8f4801bd
commit fbcc59dc67
20 changed files with 798 additions and 57 deletions

View File

@ -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<CompileStep> {
var annotation: Component = component.annotation;
var directives = annotation.template.directives;
var annotatedDirectives = ListWrapper.create();
for (var i=0; i<directives.length; i++) {
ListWrapper.push(annotatedDirectives, this._reader.read(directives[i]));
}
return createDefaultSteps(this._parser, component, annotatedDirectives);
var dirs = ListWrapper.map(component.componentDirectives, (d) => this._reader.read(d));
return createDefaultSteps(this._parser, component, dirs);
}
compile(component:Type, templateRoot:Element = null):Promise<ProtoView> {

View File

@ -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<Type>;
constructor(type:Type, annotation:Directive, shadowDomStrategy:ShadowDomStrategy) {
constructor(type:Type, annotation:Directive, shadowDomStrategy:ShadowDomStrategy,
componentDirectives:List<Type>) {
this.annotation = annotation;
this.type = type;
this.shadowDomStrategy = shadowDomStrategy;
this.componentDirectives = componentDirectives;
}
}

View File

@ -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<Type> {
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;
}
}

View File

@ -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;
}

View File

@ -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<Node>) {
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);
}
}
}

View File

@ -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<Node>;
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<Content> {
return this._collectAllContentTags(this.shadowDomView, []);
}
_collectAllContentTags(item, acc:List<Content>):List<Content> {
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<Content>, nodes:List<Node>) {
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);
}
}
}

View File

@ -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<Type>{ 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<Type> {
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<Type> {
return [];
}
}
function moveViewNodesIntoParent(parent, view) {

View File

@ -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<Node>, 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<Record>) {
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);

View File

@ -12,16 +12,18 @@ export class ViewPort {
defaultProtoView: ProtoView;
_views: List<View>;
_viewLastNode: List<Node>;
_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<Node> {
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;

View File

@ -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));
}
}

View File

@ -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]);
});
});
});
}

View File

@ -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);
});
});
});
});
}

View File

@ -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 = `<emulated-shadow-dom-component>` +
`<div>Light</div>` +
`<div template="trivial-template">DOM</div>` +
`</emulated-shadow-dom-component>`;
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 <content></content> 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 {

View File

@ -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 = `<script type="ng/content"></script>`;
export function main() {
describe('Content', function() {
it("should insert the nodes", () => {
var lightDom = new DummyLightDom();
var parent = createElement("<div><content></content></div>");
var content = DOM.firstChild(parent);
var c = new Content(lightDom, new NgElement(content));
c.insert([createElement("<a></a>"), createElement("<b></b>")])
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}<a></a><b></b>${_script}`);
});
it("should remove the nodes from the previous insertion", () => {
var lightDom = new DummyLightDom();
var parent = createElement("<div><content></content></div>");
var content = DOM.firstChild(parent);
var c = new Content(lightDom, new NgElement(content));
c.insert([createElement("<a></a>")]);
c.insert([createElement("<b></b>")]);
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}<b></b>${_script}`);
});
it("should insert empty list", () => {
var lightDom = new DummyLightDom();
var parent = createElement("<div><content></content></div>");
var content = DOM.firstChild(parent);
var c = new Content(lightDom, new NgElement(content));
c.insert([createElement("<a></a>")]);
c.insert([]);
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}${_script}`);
});
});
}
function createElement(html) {
return DOM.createTemplate(html).content.firstChild;
}

View File

@ -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("<div></div>"));
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("<div></div>"));
expect(lightDom.contentTags()).toEqual([tag]);
});
});
describe("expanded roots", () => {
it("should contain root nodes", () => {
var lightDomEl = createElement("<div><a></a></div>")
var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl);
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
});
it("should include view port nodes", () => {
var lightDomEl = createElement("<div><template></template></div>")
var template = lightDomEl.childNodes[0];
var lightDomView = new FakeView([],
MapWrapper.createFromPairs([
[template, new FakeViewPort([createElement("<a></a>")], null)]
])
);
var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl);
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
});
});
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("<div><a>1</a><b>2</b><a>3</a></div>")
var lightDom = new LightDom(lightDomView, new FakeView([
new FakeElementInjector(contentA, null),
new FakeElementInjector(contentB, null)
]), lightDomEl);
lightDom.redistribute();
expect(toHtml(contentA.nodes)).toEqual(["<a>1</a>", "<a>3</a>"]);
expect(toHtml(contentB.nodes)).toEqual(["<b>2</b>"]);
});
it("should support wildcard content tags", () => {
var wildcard = new FakeContentTag(null);
var contentB = new FakeContentTag("b");
var lightDomEl = createElement("<div><a>1</a><b>2</b><a>3</a></div>")
var lightDom = new LightDom(lightDomView, new FakeView([
new FakeElementInjector(wildcard, null),
new FakeElementInjector(contentB, null)
]), lightDomEl);
lightDom.redistribute();
expect(toHtml(wildcard.nodes)).toEqual(["<a>1</a>", "<b>2</b>", "<a>3</a>"]);
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;
}

View File

@ -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("<template></template>");
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(() => {

View File

@ -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;

View File

@ -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 {

View File

@ -47,6 +47,9 @@ class DOM {
static List<Node> 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);
}
}

View File

@ -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<childNodes.length; i++) {
res[i] = childNodes[i];
}
return res;
}
static clearNodes(el) {
el.innerHTML = "";
}
@ -49,6 +57,14 @@ export class DOM {
static removeChild(el, node) {
el.removeChild(node);
}
static insertBefore(el, node) {
el.parentNode.insertBefore(node, el);
}
static insertAllBefore(el, nodes) {
ListWrapper.forEach(nodes, (n) => {
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);
}
}