feat(emuldated_shadow_dom): implement intermediate content tags
This commit is contained in:
parent
bc8e517ae2
commit
ec8e9f5634
|
@ -325,6 +325,10 @@ export class ElementInjector extends TreeNode {
|
||||||
return pb !== _undefined && isPresent(pb);
|
return pb !== _undefined && isPresent(pb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forElement(el):boolean {
|
||||||
|
return this._preBuiltObjects.element.domElement === el;
|
||||||
|
}
|
||||||
|
|
||||||
getComponent() {
|
getComponent() {
|
||||||
if (this._proto._binding0IsComponent) {
|
if (this._proto._binding0IsComponent) {
|
||||||
return this._obj0;
|
return this._obj0;
|
||||||
|
|
|
@ -2,50 +2,98 @@ import {Decorator} from '../../annotations/annotations';
|
||||||
import {SourceLightDom, DestinationLightDom, LightDom} from './light_dom';
|
import {SourceLightDom, DestinationLightDom, LightDom} from './light_dom';
|
||||||
import {Inject} from 'di/di';
|
import {Inject} from 'di/di';
|
||||||
import {Element, Node, DOM} from 'facade/dom';
|
import {Element, Node, DOM} from 'facade/dom';
|
||||||
|
import {isPresent} from 'facade/lang';
|
||||||
import {List, ListWrapper} from 'facade/collection';
|
import {List, ListWrapper} from 'facade/collection';
|
||||||
import {NgElement} from 'core/dom/element';
|
import {NgElement} from 'core/dom/element';
|
||||||
|
|
||||||
var _scriptTemplate = DOM.createScriptTag('type', 'ng/content')
|
var _scriptTemplate = DOM.createScriptTag('type', 'ng/content')
|
||||||
|
|
||||||
@Decorator({
|
class ContentStrategy {
|
||||||
selector: 'content'
|
nodes;
|
||||||
})
|
insert(nodes:List<Nodes>){}
|
||||||
export class Content {
|
}
|
||||||
_destinationLightDom:LightDom;
|
|
||||||
|
|
||||||
_beginScript:Element;
|
/**
|
||||||
_endScript:Element;
|
* An implementation of the content tag that is used by transcluding components.
|
||||||
|
* It is used when the content tag is not a direct child of another component,
|
||||||
|
* and thus does not affect redistribution.
|
||||||
|
*/
|
||||||
|
class RenderedContent extends ContentStrategy {
|
||||||
|
beginScript:Element;
|
||||||
|
endScript:Element;
|
||||||
|
nodes:List<Node>;
|
||||||
|
|
||||||
select:string;
|
constructor(el:Element) {
|
||||||
|
this._replaceContentElementWithScriptTags(el);
|
||||||
constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) {
|
this.nodes = [];
|
||||||
this._destinationLightDom = destinationLightDom;
|
|
||||||
|
|
||||||
this.select = contentEl.getAttribute('select');
|
|
||||||
|
|
||||||
this._replaceContentElementWithScriptTags(contentEl.domElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(nodes:List<Node>) {
|
insert(nodes:List<Node>) {
|
||||||
DOM.insertAllBefore(this._endScript, nodes);
|
this.nodes = nodes;
|
||||||
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this._endScript : nodes[0]);
|
DOM.insertAllBefore(this.endScript, nodes);
|
||||||
|
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_replaceContentElementWithScriptTags(contentEl:Element) {
|
_replaceContentElementWithScriptTags(contentEl:Element) {
|
||||||
this._beginScript = DOM.clone(_scriptTemplate);
|
this.beginScript = DOM.clone(_scriptTemplate);
|
||||||
this._endScript = DOM.clone(_scriptTemplate);
|
this.endScript = DOM.clone(_scriptTemplate);
|
||||||
|
|
||||||
DOM.insertBefore(contentEl, this._beginScript);
|
DOM.insertBefore(contentEl, this.beginScript);
|
||||||
DOM.insertBefore(contentEl, this._endScript);
|
DOM.insertBefore(contentEl, this.endScript);
|
||||||
DOM.removeChild(DOM.parentElement(contentEl), contentEl);
|
DOM.removeChild(DOM.parentElement(contentEl), contentEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeNodesUntil(node:Node) {
|
_removeNodesUntil(node:Node) {
|
||||||
var p = DOM.parentElement(this._beginScript);
|
var p = DOM.parentElement(this.beginScript);
|
||||||
for (var next = DOM.nextSibling(this._beginScript);
|
for (var next = DOM.nextSibling(this.beginScript);
|
||||||
next !== node;
|
next !== node;
|
||||||
next = DOM.nextSibling(this._beginScript)) {
|
next = DOM.nextSibling(this.beginScript)) {
|
||||||
DOM.removeChild(p, next);
|
DOM.removeChild(p, next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of the content tag that is used by transcluding components.
|
||||||
|
* It is used when the content tag is a direct child of another component,
|
||||||
|
* and thus does not get rendered but only affect the distribution of its parent component.
|
||||||
|
*/
|
||||||
|
class IntermediateContent extends ContentStrategy {
|
||||||
|
destinationLightDom:LightDom;
|
||||||
|
nodes:List<Node>;
|
||||||
|
|
||||||
|
constructor(destinationLightDom:LightDom) {
|
||||||
|
this.destinationLightDom = destinationLightDom;
|
||||||
|
this.nodes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(nodes:List<Node>) {
|
||||||
|
this.nodes = nodes;
|
||||||
|
this.destinationLightDom.redistribute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Decorator({
|
||||||
|
selector: 'content'
|
||||||
|
})
|
||||||
|
export class Content {
|
||||||
|
_element:Element;
|
||||||
|
select:string;
|
||||||
|
_strategy:ContentStrategy;
|
||||||
|
|
||||||
|
constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) {
|
||||||
|
this.select = contentEl.getAttribute('select');
|
||||||
|
this._strategy = isPresent(destinationLightDom) ?
|
||||||
|
new IntermediateContent(destinationLightDom) :
|
||||||
|
new RenderedContent(contentEl.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes():List<Node> {
|
||||||
|
return this._strategy.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(nodes:List<Node>) {
|
||||||
|
this._strategy.insert(nodes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,22 +10,37 @@ import {Content} from './content_tag';
|
||||||
export class SourceLightDom {}
|
export class SourceLightDom {}
|
||||||
export class DestinationLightDom {}
|
export class DestinationLightDom {}
|
||||||
|
|
||||||
|
|
||||||
|
class _Root {
|
||||||
|
node:Node;
|
||||||
|
injector:ElementInjector;
|
||||||
|
|
||||||
|
constructor(node, injector) {
|
||||||
|
this.node = node;
|
||||||
|
this.injector = injector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: LightDom should implement SourceLightDom and DestinationLightDom
|
// TODO: LightDom should implement SourceLightDom and DestinationLightDom
|
||||||
// once interfaces are supported
|
// once interfaces are supported
|
||||||
export class LightDom {
|
export class LightDom {
|
||||||
lightDomView:View;
|
lightDomView:View;
|
||||||
shadowDomView:View;
|
shadowDomView:View;
|
||||||
roots:List<Node>;
|
nodes:List<Node>;
|
||||||
|
roots:List<_Root>;
|
||||||
|
|
||||||
constructor(lightDomView:View, shadowDomView:View, element:Element) {
|
constructor(lightDomView:View, shadowDomView:View, element:Element) {
|
||||||
this.lightDomView = lightDomView;
|
this.lightDomView = lightDomView;
|
||||||
this.shadowDomView = shadowDomView;
|
this.shadowDomView = shadowDomView;
|
||||||
this.roots = DOM.childNodesAsList(element);
|
this.nodes = DOM.childNodesAsList(element);
|
||||||
DOM.clearNodes(element);
|
this.roots = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
redistribute() {
|
redistribute() {
|
||||||
redistributeNodes(this.contentTags(), this.expandedDomNodes());
|
var tags = this.contentTags();
|
||||||
|
if (isPresent(tags)) {
|
||||||
|
redistributeNodes(tags, this.expandedDomNodes());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentTags(): List<Content> {
|
contentTags(): List<Content> {
|
||||||
|
@ -33,7 +48,11 @@ export class LightDom {
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectAllContentTags(item, acc:List<Content>):List<Content> {
|
_collectAllContentTags(item, acc:List<Content>):List<Content> {
|
||||||
ListWrapper.forEach(item.elementInjectors, (ei) => {
|
var eis = item.elementInjectors;
|
||||||
|
for (var i = 0; i < eis.length; ++i) {
|
||||||
|
var ei = eis[i];
|
||||||
|
if (isBlank(ei)) continue;
|
||||||
|
|
||||||
if (ei.hasDirective(Content)) {
|
if (ei.hasDirective(Content)) {
|
||||||
ListWrapper.push(acc, ei.get(Content));
|
ListWrapper.push(acc, ei.get(Content));
|
||||||
|
|
||||||
|
@ -43,23 +62,43 @@ export class LightDom {
|
||||||
this._collectAllContentTags(c, acc);
|
this._collectAllContentTags(c, acc);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
expandedDomNodes():List {
|
expandedDomNodes():List {
|
||||||
var res = [];
|
var res = [];
|
||||||
ListWrapper.forEach(this.roots, (root) => {
|
|
||||||
// TODO: vsavkin calculcate this info statically when creating light dom
|
var roots = this._roots();
|
||||||
var viewPort = this.lightDomView.getViewPortByTemplateElement(root);
|
for (var i = 0; i < roots.length; ++i) {
|
||||||
if (isPresent(viewPort)) {
|
|
||||||
res = ListWrapper.concat(res, viewPort.nodes());
|
var root = roots[i];
|
||||||
|
var ei = root.injector;
|
||||||
|
|
||||||
|
if (isPresent(ei) && ei.hasPreBuiltObject(ViewPort)) {
|
||||||
|
var vp = root.injector.get(ViewPort);
|
||||||
|
res = ListWrapper.concat(res, vp.nodes());
|
||||||
|
|
||||||
|
} else if (isPresent(ei) && ei.hasDirective(Content)) {
|
||||||
|
var content = root.injector.get(Content);
|
||||||
|
res = ListWrapper.concat(res, content.nodes());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ListWrapper.push(res, root);
|
ListWrapper.push(res, root.node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_roots() {
|
||||||
|
if (isPresent(this.roots)) return this.roots;
|
||||||
|
|
||||||
|
var viewInj = this.lightDomView.elementInjectors;
|
||||||
|
this.roots = ListWrapper.map(this.nodes, (n) =>
|
||||||
|
new _Root(n, ListWrapper.find(viewInj, (inj) => inj.forElement(n))));
|
||||||
|
|
||||||
|
return this.roots;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function redistributeNodes(contents:List<Content>, nodes:List<Node>) {
|
function redistributeNodes(contents:List<Content>, nodes:List<Node>) {
|
||||||
|
|
|
@ -155,9 +155,17 @@ export class View {
|
||||||
if (isPresent(componentDirective)) {
|
if (isPresent(componentDirective)) {
|
||||||
this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector,
|
this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector,
|
||||||
elementInjector, elementInjector.getComponent());
|
elementInjector, elementInjector.getComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should be moved into DOM write queue
|
||||||
|
for (var i = 0; i < binders.length; ++i) {
|
||||||
|
var componentDirective = binders[i].componentDirective;
|
||||||
|
if (isPresent(componentDirective)) {
|
||||||
var lightDom = this.preBuiltObjects[i].lightDom;
|
var lightDom = this.preBuiltObjects[i].lightDom;
|
||||||
if (isPresent(lightDom)) lightDom.redistribute();
|
if (isPresent(lightDom)) {
|
||||||
|
lightDom.redistribute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,16 +200,6 @@ 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>) {
|
_invokeMementoForRecords(records:List<Record>) {
|
||||||
for(var i = 0; i < records.length; ++i) {
|
for(var i = 0; i < records.length; ++i) {
|
||||||
this._invokeMementoFor(records[i]);
|
this._invokeMementoFor(records[i]);
|
||||||
|
|
|
@ -109,48 +109,7 @@ export function main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emulate content tag', (done) => {
|
|
||||||
var temp = `<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, el(temp)).
|
|
||||||
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({
|
@Decorator({
|
||||||
|
@ -166,7 +125,7 @@ class MyDir {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: new TemplateConfig({
|
template: new TemplateConfig({
|
||||||
directives: [MyDir, ChildComp, SomeTemplate, EmulatedShadowDomCmp, TrivialTemplateDirective]
|
directives: [MyDir, ChildComp, SomeTemplate]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
class MyComp {
|
class MyComp {
|
||||||
|
@ -207,3 +166,4 @@ class MyService {
|
||||||
this.greeting = 'hello';
|
this.greeting = 'hello';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,22 +14,20 @@ var _script = `<script type="ng/content"></script>`;
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('Content', function() {
|
describe('Content', function() {
|
||||||
it("should insert the nodes", () => {
|
it("should insert the nodes", () => {
|
||||||
var lightDom = new DummyLightDom();
|
|
||||||
var parent = el("<div><content></content></div>");
|
var parent = el("<div><content></content></div>");
|
||||||
var content = DOM.firstChild(parent);
|
var content = DOM.firstChild(parent);
|
||||||
|
|
||||||
var c = new Content(lightDom, new NgElement(content));
|
var c = new Content(null, new NgElement(content));
|
||||||
c.insert([el("<a></a>"), el("<b></b>")])
|
c.insert([el("<a></a>"), el("<b></b>")])
|
||||||
|
|
||||||
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}<a></a><b></b>${_script}`);
|
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}<a></a><b></b>${_script}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove the nodes from the previous insertion", () => {
|
it("should remove the nodes from the previous insertion", () => {
|
||||||
var lightDom = new DummyLightDom();
|
|
||||||
var parent = el("<div><content></content></div>");
|
var parent = el("<div><content></content></div>");
|
||||||
var content = DOM.firstChild(parent);
|
var content = DOM.firstChild(parent);
|
||||||
|
|
||||||
var c = new Content(lightDom, new NgElement(content));
|
var c = new Content(null, new NgElement(content));
|
||||||
c.insert([el("<a></a>")]);
|
c.insert([el("<a></a>")]);
|
||||||
c.insert([el("<b></b>")]);
|
c.insert([el("<b></b>")]);
|
||||||
|
|
||||||
|
@ -37,11 +35,10 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should insert empty list", () => {
|
it("should insert empty list", () => {
|
||||||
var lightDom = new DummyLightDom();
|
|
||||||
var parent = el("<div><content></content></div>");
|
var parent = el("<div><content></content></div>");
|
||||||
var content = DOM.firstChild(parent);
|
var content = DOM.firstChild(parent);
|
||||||
|
|
||||||
var c = new Content(lightDom, new NgElement(content));
|
var c = new Content(null, new NgElement(content));
|
||||||
c.insert([el("<a></a>")]);
|
c.insert([el("<a></a>")]);
|
||||||
c.insert([]);
|
c.insert([]);
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,12 @@ import {ProtoRecordRange} from 'change_detection/change_detection';
|
||||||
class FakeElementInjector {
|
class FakeElementInjector {
|
||||||
content;
|
content;
|
||||||
viewPort;
|
viewPort;
|
||||||
|
element;
|
||||||
|
|
||||||
constructor(content, viewPort) {
|
constructor(content = null, viewPort = null, element = null) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.viewPort = viewPort;
|
this.viewPort = viewPort;
|
||||||
|
this.element = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasDirective(type) {
|
hasDirective(type) {
|
||||||
|
@ -29,6 +31,10 @@ class FakeElementInjector {
|
||||||
return this.viewPort != null;
|
return this.viewPort != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forElement(n) {
|
||||||
|
return this.element == n;
|
||||||
|
}
|
||||||
|
|
||||||
get(t) {
|
get(t) {
|
||||||
if (t === Content) return this.content;
|
if (t === Content) return this.content;
|
||||||
if (t === ViewPort) return this.viewPort;
|
if (t === ViewPort) return this.viewPort;
|
||||||
|
@ -44,16 +50,9 @@ class FakeElementInjector {
|
||||||
@IMPLEMENTS(View)
|
@IMPLEMENTS(View)
|
||||||
class FakeView {
|
class FakeView {
|
||||||
elementInjectors;
|
elementInjectors;
|
||||||
ports;
|
|
||||||
|
|
||||||
constructor(elementInjectors = null, ports = null) {
|
constructor(elementInjectors = null) {
|
||||||
this.elementInjectors = elementInjectors;
|
this.elementInjectors = elementInjectors;
|
||||||
this.ports = ports;
|
|
||||||
}
|
|
||||||
|
|
||||||
getViewPortByTemplateElement(el) {
|
|
||||||
if (isBlank(this.ports)) return null;
|
|
||||||
return MapWrapper.get(this.ports, el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
noSuchMethod(i) {
|
noSuchMethod(i) {
|
||||||
|
@ -67,7 +66,7 @@ class FakeViewPort {
|
||||||
_nodes;
|
_nodes;
|
||||||
_contentTagContainers;
|
_contentTagContainers;
|
||||||
|
|
||||||
constructor(nodes, views) {
|
constructor(nodes = null, views = null) {
|
||||||
this._nodes = nodes;
|
this._nodes = nodes;
|
||||||
this._contentTagContainers = views;
|
this._contentTagContainers = views;
|
||||||
}
|
}
|
||||||
|
@ -90,14 +89,19 @@ class FakeViewPort {
|
||||||
@IMPLEMENTS(Content)
|
@IMPLEMENTS(Content)
|
||||||
class FakeContentTag {
|
class FakeContentTag {
|
||||||
select;
|
select;
|
||||||
nodes;
|
_nodes;
|
||||||
|
|
||||||
constructor(select = null) {
|
constructor(select = null, nodes = null) {
|
||||||
this.select = select;
|
this.select = select;
|
||||||
|
this._nodes = nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(nodes){
|
insert(nodes){
|
||||||
this.nodes = ListWrapper.clone(nodes);
|
this._nodes = ListWrapper.clone(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes() {
|
||||||
|
return this._nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
noSuchMethod(i) {
|
noSuchMethod(i) {
|
||||||
|
@ -111,13 +115,13 @@ export function main() {
|
||||||
var lightDomView;
|
var lightDomView;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
lightDomView = new FakeView([], MapWrapper.create());
|
lightDomView = new FakeView([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("contentTags", () => {
|
describe("contentTags", () => {
|
||||||
it("should collect content tags from element injectors", () => {
|
it("should collect content tags from element injectors", () => {
|
||||||
var tag = new FakeContentTag();
|
var tag = new FakeContentTag();
|
||||||
var shadowDomView = new FakeView([new FakeElementInjector(tag, null)]);
|
var shadowDomView = new FakeView([new FakeElementInjector(tag)]);
|
||||||
|
|
||||||
var lightDom = new LightDom(lightDomView, shadowDomView, el("<div></div>"));
|
var lightDom = new LightDom(lightDomView, shadowDomView, el("<div></div>"));
|
||||||
|
|
||||||
|
@ -147,15 +151,34 @@ export function main() {
|
||||||
|
|
||||||
it("should include view port nodes", () => {
|
it("should include view port nodes", () => {
|
||||||
var lightDomEl = el("<div><template></template></div>")
|
var lightDomEl = el("<div><template></template></div>")
|
||||||
var template = lightDomEl.childNodes[0];
|
|
||||||
|
|
||||||
var lightDomView = new FakeView([],
|
var lightDomView = new FakeView([
|
||||||
MapWrapper.createFromPairs([
|
new FakeElementInjector(
|
||||||
[template, new FakeViewPort([el("<a></a>")], null)]
|
null,
|
||||||
])
|
new FakeViewPort([el("<a></a>")]),
|
||||||
);
|
DOM.firstChild(lightDomEl))]);
|
||||||
|
|
||||||
var lightDom = new LightDom(lightDomView, new FakeView(), lightDomEl);
|
var lightDom = new LightDom(
|
||||||
|
lightDomView,
|
||||||
|
new FakeView(),
|
||||||
|
lightDomEl);
|
||||||
|
|
||||||
|
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include content nodes", () => {
|
||||||
|
var lightDomEl = el("<div><content></content></div>")
|
||||||
|
|
||||||
|
var lightDomView = new FakeView([
|
||||||
|
new FakeElementInjector(
|
||||||
|
new FakeContentTag(null, [el("<a></a>")]),
|
||||||
|
null,
|
||||||
|
DOM.firstChild(lightDomEl))]);
|
||||||
|
|
||||||
|
var lightDom = new LightDom(
|
||||||
|
lightDomView,
|
||||||
|
new FakeView(),
|
||||||
|
lightDomEl);
|
||||||
|
|
||||||
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
|
expect(toHtml(lightDom.expandedDomNodes())).toEqual(["<a></a>"]);
|
||||||
});
|
});
|
||||||
|
@ -175,8 +198,8 @@ export function main() {
|
||||||
|
|
||||||
lightDom.redistribute();
|
lightDom.redistribute();
|
||||||
|
|
||||||
expect(toHtml(contentA.nodes)).toEqual(["<a>1</a>", "<a>3</a>"]);
|
expect(toHtml(contentA.nodes())).toEqual(["<a>1</a>", "<a>3</a>"]);
|
||||||
expect(toHtml(contentB.nodes)).toEqual(["<b>2</b>"]);
|
expect(toHtml(contentB.nodes())).toEqual(["<b>2</b>"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support wildcard content tags", () => {
|
it("should support wildcard content tags", () => {
|
||||||
|
@ -192,8 +215,8 @@ export function main() {
|
||||||
|
|
||||||
lightDom.redistribute();
|
lightDom.redistribute();
|
||||||
|
|
||||||
expect(toHtml(wildcard.nodes)).toEqual(["<a>1</a>", "<b>2</b>", "<a>3</a>"]);
|
expect(toHtml(wildcard.nodes())).toEqual(["<a>1</a>", "<b>2</b>", "<a>3</a>"]);
|
||||||
expect(toHtml(contentB.nodes)).toEqual([]);
|
expect(toHtml(contentB.nodes())).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_lib/test_lib';
|
||||||
|
|
||||||
|
import {DOM} from 'facade/dom';
|
||||||
|
|
||||||
|
import {Injector} from 'di/di';
|
||||||
|
import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection';
|
||||||
|
|
||||||
|
import {Compiler, CompilerCache} from 'core/compiler/compiler';
|
||||||
|
import {LifeCycle} from 'core/life_cycle/life_cycle';
|
||||||
|
import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader';
|
||||||
|
import {ShadowDomStrategy, ShadowDomNative, ShadowDomEmulated} from 'core/compiler/shadow_dom';
|
||||||
|
|
||||||
|
import {Decorator, Component, Template} from 'core/annotations/annotations';
|
||||||
|
import {TemplateConfig} from 'core/annotations/template_config';
|
||||||
|
|
||||||
|
import {ViewPort} from 'core/compiler/viewport';
|
||||||
|
import {StringMapWrapper, MapWrapper} from 'facade/collection';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe('integration tests', function() {
|
||||||
|
|
||||||
|
StringMapWrapper.forEach(
|
||||||
|
{"native" : ShadowDomNative, "emulated" : ShadowDomEmulated}, (strategy, name) => {
|
||||||
|
|
||||||
|
describe(`${name} shadow dom strategy`, () => {
|
||||||
|
var compiler;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
compiler = new Compiler(null, new TestDirectiveMetadataReader(strategy),
|
||||||
|
new Parser(new Lexer()), new CompilerCache());
|
||||||
|
});
|
||||||
|
|
||||||
|
function compile(template, assertions) {
|
||||||
|
compiler.compile(MyComp, el(template)).
|
||||||
|
then(createView).
|
||||||
|
then((view) => {
|
||||||
|
var lc = new LifeCycle(new ChangeDetector(view.recordRange));
|
||||||
|
assertions(view, lc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should support multiple content tags', (done) => {
|
||||||
|
var temp = '<multiple-content-tags>' +
|
||||||
|
'<div>B</div>' +
|
||||||
|
'<div>C</div>' +
|
||||||
|
'<div class="left">A</div>' +
|
||||||
|
'</multiple-content-tags>';
|
||||||
|
|
||||||
|
compile(temp, (view, lc) => {
|
||||||
|
expect(view.nodes).toHaveText('(A, BC)');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redistribute only direct children', (done) => {
|
||||||
|
var temp = '<multiple-content-tags>' +
|
||||||
|
'<div>B<div class="left">A</div></div>' +
|
||||||
|
'<div>C</div>' +
|
||||||
|
'</multiple-content-tags>';
|
||||||
|
|
||||||
|
compile(temp, (view, lc) => {
|
||||||
|
expect(view.nodes).toHaveText('(, BAC)');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redistribute when the light dom changes", (done) => {
|
||||||
|
var temp = '<multiple-content-tags>' +
|
||||||
|
'<div template="manual" class="left">A</div>' +
|
||||||
|
'<div>B</div>' +
|
||||||
|
'</multiple-content-tags>';
|
||||||
|
|
||||||
|
compile(temp, (view, lc) => {
|
||||||
|
var dir = view.elementInjectors[1].get(ManualTemplateDirective);
|
||||||
|
|
||||||
|
expect(view.nodes).toHaveText('(, B)');
|
||||||
|
|
||||||
|
dir.show();
|
||||||
|
lc.tick();
|
||||||
|
|
||||||
|
expect(view.nodes).toHaveText('(A, B)');
|
||||||
|
|
||||||
|
dir.hide();
|
||||||
|
lc.tick();
|
||||||
|
|
||||||
|
expect(view.nodes).toHaveText('(, B)');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support nested components", (done) => {
|
||||||
|
var temp = '<outer-with-indirect-nested>' +
|
||||||
|
'<div>A</div>' +
|
||||||
|
'<div>B</div>' +
|
||||||
|
'</outer-with-indirect-nested>';
|
||||||
|
|
||||||
|
compile(temp, (view, lc) => {
|
||||||
|
expect(view.nodes).toHaveText('OUTER(SIMPLE(AB))');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support nesting with content being direct child of a nested component", (done) => {
|
||||||
|
var temp = '<outer>' +
|
||||||
|
'<div template="manual" class="left">A</div>' +
|
||||||
|
'<div>B</div>' +
|
||||||
|
'<div>C</div>' +
|
||||||
|
'</outer>';
|
||||||
|
|
||||||
|
compile(temp, (view, lc) => {
|
||||||
|
var dir = view.elementInjectors[1].get(ManualTemplateDirective);
|
||||||
|
|
||||||
|
expect(view.nodes).toHaveText('OUTER(INNER(INNERINNER(,BC)))');
|
||||||
|
|
||||||
|
dir.show();
|
||||||
|
lc.tick();
|
||||||
|
|
||||||
|
expect(view.nodes).toHaveText('OUTER(INNER(INNERINNER(A,BC)))');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable once dom-write queue is implemented and onDehydrate is implemented
|
||||||
|
//it('should redistribute when the shadow dom changes', (done) => {
|
||||||
|
// var temp = '<conditional-content>' +
|
||||||
|
// '<div class="left">A</div>' +
|
||||||
|
// '<div>B</div>' +
|
||||||
|
// '<div>C</div>' +
|
||||||
|
// '</conditional-content>';
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// compile(temp, (view, lc) => {
|
||||||
|
// var cmp = view.elementInjectors[0].get(ConditionalContentComponent);
|
||||||
|
//
|
||||||
|
// expect(view.nodes).toHaveText('(, ABC)');
|
||||||
|
//
|
||||||
|
// cmp.showLeft();
|
||||||
|
// lc.tick();
|
||||||
|
//
|
||||||
|
// expect(view.nodes).toHaveText('(A, BC)');
|
||||||
|
//
|
||||||
|
// cmp.hideLeft()
|
||||||
|
// lc.tick();
|
||||||
|
//
|
||||||
|
// expect(view.nodes).toHaveText('(, ABC)');
|
||||||
|
//
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
//});
|
||||||
|
|
||||||
|
//Implement once NgElement support changing a class
|
||||||
|
//it("should redistribute when a class has been added or removed");
|
||||||
|
//it('should not lose focus', () => {
|
||||||
|
// var temp = `<simple>aaa<input type="text" id="focused-input" ng-class="{'aClass' : showClass}"> bbb</simple>`;
|
||||||
|
//
|
||||||
|
// compile(temp, (view, lc) => {
|
||||||
|
// var input = view.nodes[1];
|
||||||
|
// input.focus();
|
||||||
|
//
|
||||||
|
// expect(document.activeElement.id).toEqual("focused-input");
|
||||||
|
//
|
||||||
|
// // update class of input
|
||||||
|
//
|
||||||
|
// expect(document.activeElement.id).toEqual("focused-input");
|
||||||
|
// });
|
||||||
|
//});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestDirectiveMetadataReader extends DirectiveMetadataReader {
|
||||||
|
shadowDomStrategy;
|
||||||
|
|
||||||
|
constructor(shadowDomStrategy) {
|
||||||
|
this.shadowDomStrategy = shadowDomStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseShadowDomStrategy(annotation:Component):ShadowDomStrategy{
|
||||||
|
return this.shadowDomStrategy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Template({
|
||||||
|
selector: '[manual]'
|
||||||
|
})
|
||||||
|
class ManualTemplateDirective {
|
||||||
|
viewPort;
|
||||||
|
constructor(viewPort:ViewPort) {
|
||||||
|
this.viewPort = viewPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
show() { this.viewPort.create(); }
|
||||||
|
hide() { this.viewPort.remove(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Template({
|
||||||
|
selector: '[auto]',
|
||||||
|
bind: {
|
||||||
|
'auto': 'auto'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class AutoTemplateDirective {
|
||||||
|
viewPort;
|
||||||
|
constructor(viewPort:ViewPort) {
|
||||||
|
this.viewPort = viewPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
set auto(newValue:boolean) {
|
||||||
|
if (newValue) {
|
||||||
|
this.viewPort.create();
|
||||||
|
} else {
|
||||||
|
this.viewPort.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'simple',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: 'SIMPLE(<content></content>)'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class Simple {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'multiple-content-tags',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: '(<content select=".left"></content>, <content></content>)'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class MultipleContentTagsComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'conditional-content',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: '<div>(<div template="auto: cond"><content select=".left"></content></div>, <content></content>)</div>',
|
||||||
|
directives: [AutoTemplateDirective]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class ConditionalContentComponent {
|
||||||
|
cond:boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cond = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLeft() { this.cond = true; }
|
||||||
|
hideLeft() { this.cond = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'outer-with-indirect-nested',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: 'OUTER(<simple><div><content></content></div></simple>)',
|
||||||
|
directives: [Simple]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class OuterWithIndirectNestedComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'outer',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: 'OUTER(<inner><content></content></inner>)',
|
||||||
|
directives: [InnerComponent]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class OuterComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'inner',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: 'INNER(<innerinner><content></content></innerinner>)',
|
||||||
|
directives: [InnerInnerComponent]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class InnerComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'innerinner',
|
||||||
|
template: new TemplateConfig({
|
||||||
|
inline: 'INNERINNER(<content select=".left"></content>,<content></content>)'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class InnerInnerComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: new TemplateConfig({
|
||||||
|
directives: [MultipleContentTagsComponent, ManualTemplateDirective,
|
||||||
|
ConditionalContentComponent, OuterWithIndirectNestedComponent, OuterComponent]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
class MyComp {
|
||||||
|
}
|
||||||
|
|
||||||
|
function createView(pv) {
|
||||||
|
var view = pv.instantiate(null);
|
||||||
|
view.hydrate(new Injector([]), null, {});
|
||||||
|
return view;
|
||||||
|
}
|
|
@ -69,25 +69,6 @@ export function main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getViewPortByTemplateElement", () => {
|
|
||||||
var view, viewPort, templateElement;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
templateElement = el("<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() {
|
describe('with locals', function() {
|
||||||
var view;
|
var view;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -422,37 +403,6 @@ export function main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('event handlers', () => {
|
|
||||||
var view, ctx, called;
|
|
||||||
|
|
||||||
function createViewAndContext(protoView) {
|
|
||||||
view = createView(protoView);
|
|
||||||
ctx = view.context;
|
|
||||||
called = 0;
|
|
||||||
ctx.callMe = () => called += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dispatchClick(el) {
|
|
||||||
DOM.dispatchEvent(el, DOM.createMouseEvent('click'));
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should fire on non-bubbling native events', () => {
|
|
||||||
var pv = new ProtoView(el('<div class="ng-binding"><div></div></div>'),
|
|
||||||
new ProtoRecordRange());
|
|
||||||
pv.bindElement(null);
|
|
||||||
pv.bindEvent('click', parser.parseBinding('callMe()', null));
|
|
||||||
createViewAndContext(pv);
|
|
||||||
|
|
||||||
dispatchClick(view.nodes[0]);
|
|
||||||
dispatchClick(view.nodes[0].firstChild);
|
|
||||||
|
|
||||||
// the bubbled event does not execute the expression.
|
|
||||||
// It is trivially passing on webkit browsers due to
|
|
||||||
// https://bugs.webkit.org/show_bug.cgi?id=122755
|
|
||||||
expect(called).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('react to record changes', () => {
|
describe('react to record changes', () => {
|
||||||
var view, cd, ctx;
|
var view, cd, ctx;
|
||||||
|
|
||||||
|
@ -633,7 +583,6 @@ class MyEvaluationContext {
|
||||||
foo:string;
|
foo:string;
|
||||||
a;
|
a;
|
||||||
b;
|
b;
|
||||||
callMe;
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.foo = 'bar';
|
this.foo = 'bar';
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,6 +36,7 @@ class NotExpect extends gns.NotExpect {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(fn) {
|
beforeEach(fn) {
|
||||||
|
gns.guinnessEnableHtmlMatchers();
|
||||||
gns.beforeEach(_enableReflection(fn));
|
gns.beforeEach(_enableReflection(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,20 @@ window.beforeEach(function() {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toHaveText: function() {
|
||||||
|
return {
|
||||||
|
compare: function(actual, expectedText) {
|
||||||
|
var actualText = elementText(actual);
|
||||||
|
return {
|
||||||
|
pass: actualText == expectedText,
|
||||||
|
get message() {
|
||||||
|
return 'Expected ' + actualText + ' to be equal to ' + expectedText;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
toImplement: function() {
|
toImplement: function() {
|
||||||
return {
|
return {
|
||||||
compare: function(actualObject, expectedInterface) {
|
compare: function(actualObject, expectedInterface) {
|
||||||
|
@ -131,6 +145,20 @@ function mapToString(m) {
|
||||||
return `{ ${res.join(',')} }`;
|
return `{ ${res.join(',')} }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function elementText(n) {
|
||||||
|
var hasShadowRoot = (n) => n instanceof Element && n.shadowRoot;
|
||||||
|
var hasNodes = (n) => n.childNodes && n.childNodes.length > 0;
|
||||||
|
|
||||||
|
if (n instanceof Comment) return '';
|
||||||
|
|
||||||
|
if (n instanceof Array) return n.map((nn) => elementText(nn)).join("");
|
||||||
|
if (n instanceof Element && n.tagName == 'CONTENT')
|
||||||
|
return elementText(Array.prototype.slice.apply(n.getDistributedNodes()));
|
||||||
|
if (hasShadowRoot(n)) return elementText(DOM.childNodesAsList(n.shadowRoot));
|
||||||
|
if (hasNodes(n)) return elementText(DOM.childNodesAsList(n));
|
||||||
|
|
||||||
|
return n.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
export function el(html) {
|
export function el(html) {
|
||||||
return DOM.createTemplate(html).content.firstChild;
|
return DOM.createTemplate(html).content.firstChild;
|
||||||
|
|
Loading…
Reference in New Issue