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

@ -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(() => {