feat(render): add generic view factory based on the template commands
Part of #3605 Closes #4367
This commit is contained in:
parent
0ed6fc4f6b
commit
1cf45757cd
|
@ -158,7 +158,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
removeChild(el, node) { el.removeChild(node); }
|
||||
replaceChild(el: Node, newChild, oldChild) { el.replaceChild(newChild, oldChild); }
|
||||
remove(node): Node {
|
||||
node.parentNode.removeChild(node);
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
insertBefore(el, node) { el.parentNode.insertBefore(node, el); }
|
||||
|
|
|
@ -221,9 +221,7 @@ class Html5LibDomAdapter implements DomAdapter {
|
|||
return new Element.tag(tagName);
|
||||
}
|
||||
|
||||
createTextNode(String text, [doc]) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
createTextNode(String text, [doc]) => new Text(text);
|
||||
|
||||
createScriptTag(String attrName, String attrValue, [doc]) {
|
||||
throw 'not implemented';
|
||||
|
|
|
@ -276,7 +276,11 @@ export class Parse5DomAdapter extends DomAdapter {
|
|||
createElement(tagName): HTMLElement {
|
||||
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []);
|
||||
}
|
||||
createTextNode(text: string): Text { throw _notImplemented('createTextNode'); }
|
||||
createTextNode(text: string): Text {
|
||||
var t = <any>this.createComment(text);
|
||||
t.type = 'text';
|
||||
return t;
|
||||
}
|
||||
createScriptTag(attrName: string, attrValue: string): HTMLElement {
|
||||
return treeAdapter.createElement("script", 'http://www.w3.org/1999/xhtml',
|
||||
[{name: attrName, value: attrValue}]);
|
||||
|
@ -424,6 +428,9 @@ export class Parse5DomAdapter extends DomAdapter {
|
|||
setAttribute(element, attribute: string, value: string) {
|
||||
if (attribute) {
|
||||
element.attribs[attribute] = value;
|
||||
if (attribute === 'class') {
|
||||
element.className = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
removeAttribute(element, attribute: string) {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import {BaseException} from 'angular2/src/core/facade/exceptions';
|
||||
import {ListWrapper, MapWrapper, Map, StringMapWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {isPresent, isBlank, stringify} from 'angular2/src/core/facade/lang';
|
||||
|
||||
import {
|
||||
RenderViewRef,
|
||||
RenderEventDispatcher,
|
||||
RenderTemplateCmd,
|
||||
RenderProtoViewRef,
|
||||
RenderFragmentRef
|
||||
} from './api';
|
||||
|
||||
export class DefaultProtoViewRef extends RenderProtoViewRef {
|
||||
constructor(public cmds: RenderTemplateCmd[]) { super(); }
|
||||
}
|
||||
|
||||
export class DefaultRenderFragmentRef<N> extends RenderFragmentRef {
|
||||
constructor(public nodes: N[]) { super(); }
|
||||
}
|
||||
|
||||
export class DefaultRenderView<N> extends RenderViewRef {
|
||||
hydrated: boolean = false;
|
||||
eventDispatcher: RenderEventDispatcher = null;
|
||||
globalEventRemovers: Function[] = null;
|
||||
|
||||
constructor(public fragments: DefaultRenderFragmentRef<N>[], public boundTextNodes: N[],
|
||||
public boundElements: N[], public nativeShadowRoots: N[],
|
||||
public globalEventAdders: Function[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
hydrate() {
|
||||
if (this.hydrated) throw new BaseException('The view is already hydrated.');
|
||||
this.hydrated = true;
|
||||
this.globalEventRemovers = ListWrapper.createFixedSize(this.globalEventAdders.length);
|
||||
for (var i = 0; i < this.globalEventAdders.length; i++) {
|
||||
this.globalEventRemovers[i] = this.globalEventAdders[i]();
|
||||
}
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
if (!this.hydrated) throw new BaseException('The view is already dehydrated.');
|
||||
for (var i = 0; i < this.globalEventRemovers.length; i++) {
|
||||
this.globalEventRemovers[i]();
|
||||
}
|
||||
this.globalEventRemovers = null;
|
||||
this.hydrated = false;
|
||||
}
|
||||
|
||||
setEventDispatcher(dispatcher: RenderEventDispatcher) { this.eventDispatcher = dispatcher; }
|
||||
|
||||
dispatchRenderEvent(boundElementIndex: number, eventName: string, event: any): boolean {
|
||||
var allowDefaultBehavior = true;
|
||||
if (isPresent(this.eventDispatcher)) {
|
||||
var locals = new Map();
|
||||
locals.set('$event', event);
|
||||
allowDefaultBehavior =
|
||||
this.eventDispatcher.dispatchRenderEvent(boundElementIndex, eventName, locals);
|
||||
}
|
||||
return allowDefaultBehavior;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
import {isBlank, isPresent} from 'angular2/src/core/facade/lang';
|
||||
import {
|
||||
RenderEventDispatcher,
|
||||
RenderTemplateCmd,
|
||||
RenderCommandVisitor,
|
||||
RenderBeginElementCmd,
|
||||
RenderBeginComponentCmd,
|
||||
RenderNgContentCmd,
|
||||
RenderTextCmd,
|
||||
RenderEmbeddedTemplateCmd
|
||||
} from './api';
|
||||
import {DefaultRenderView, DefaultRenderFragmentRef} from './view';
|
||||
|
||||
export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceElement: any,
|
||||
nodeFactory: NodeFactory<any>): DefaultRenderView<any> {
|
||||
var builders: RenderViewBuilder<any>[] = [];
|
||||
visitAll(new RenderViewBuilder<any>(null, null, inplaceElement, builders, nodeFactory),
|
||||
fragmentCmds);
|
||||
var boundElements: any[] = [];
|
||||
var boundTextNodes: any[] = [];
|
||||
var nativeShadowRoots: any[] = [];
|
||||
var fragments: DefaultRenderFragmentRef<any>[] = [];
|
||||
var viewElementOffset = 0;
|
||||
var view: DefaultRenderView<any>;
|
||||
var eventDispatcher = (boundElementIndex: number, eventName: string, event: any) =>
|
||||
view.dispatchRenderEvent(boundElementIndex, eventName, event);
|
||||
var globalEventAdders: Function[] = [];
|
||||
|
||||
for (var i = 0; i < builders.length; i++) {
|
||||
var builder = builders[i];
|
||||
addAll(builder.boundElements, boundElements);
|
||||
addAll(builder.boundTextNodes, boundTextNodes);
|
||||
addAll(builder.nativeShadowRoots, nativeShadowRoots);
|
||||
if (isBlank(builder.rootNodesParent)) {
|
||||
fragments.push(new DefaultRenderFragmentRef<any>(builder.fragmentRootNodes));
|
||||
}
|
||||
for (var j = 0; j < builder.eventData.length; j++) {
|
||||
var eventData = builder.eventData[j];
|
||||
var boundElementIndex = eventData[0] + viewElementOffset;
|
||||
var target = eventData[1];
|
||||
var eventName = eventData[2];
|
||||
if (isPresent(target)) {
|
||||
var handler =
|
||||
createEventHandler(boundElementIndex, `${target}:${eventName}`, eventDispatcher);
|
||||
globalEventAdders.push(createGlobalEventAdder(target, eventName, handler, nodeFactory));
|
||||
} else {
|
||||
var handler = createEventHandler(boundElementIndex, eventName, eventDispatcher);
|
||||
nodeFactory.on(boundElements[boundElementIndex], eventName, handler);
|
||||
}
|
||||
}
|
||||
viewElementOffset += builder.boundElements.length;
|
||||
}
|
||||
view = new DefaultRenderView<any>(fragments, boundTextNodes, boundElements, nativeShadowRoots,
|
||||
globalEventAdders);
|
||||
return view;
|
||||
}
|
||||
|
||||
function createEventHandler(boundElementIndex: number, eventName: string,
|
||||
eventDispatcher: Function): Function {
|
||||
return ($event) => eventDispatcher(boundElementIndex, eventName, $event);
|
||||
}
|
||||
|
||||
function createGlobalEventAdder(target: string, eventName: string, eventHandler: Function,
|
||||
nodeFactory: NodeFactory<any>): Function {
|
||||
return () => nodeFactory.globalOn(target, eventName, eventHandler);
|
||||
}
|
||||
|
||||
export interface NodeFactory<N> {
|
||||
resolveComponentTemplate(templateId: number): RenderTemplateCmd[];
|
||||
createTemplateAnchor(attrNameAndValues: string[]): N;
|
||||
createElement(name: string, attrNameAndValues: string[]): N;
|
||||
mergeElement(existing: N, attrNameAndValues: string[]);
|
||||
createShadowRoot(host: N): N;
|
||||
createText(value: string): N;
|
||||
appendChild(parent: N, child: N);
|
||||
on(element: N, eventName: string, callback: Function);
|
||||
globalOn(target: string, eventName: string, callback: Function): Function;
|
||||
}
|
||||
|
||||
class RenderViewBuilder<N> implements RenderCommandVisitor {
|
||||
parentStack: Array<N | Component<N>>;
|
||||
boundTextNodes: N[] = [];
|
||||
boundElements: N[] = [];
|
||||
eventData: any[][] = [];
|
||||
|
||||
fragmentRootNodes: N[] = [];
|
||||
nativeShadowRoots: N[] = [];
|
||||
|
||||
constructor(public parentComponent: Component<N>, public rootNodesParent: N,
|
||||
public inplaceElement: N, public allBuilders: RenderViewBuilder<N>[],
|
||||
public factory: NodeFactory<N>) {
|
||||
this.parentStack = [rootNodesParent];
|
||||
allBuilders.push(this);
|
||||
}
|
||||
|
||||
get parent(): N | Component<N> { return this.parentStack[this.parentStack.length - 1]; }
|
||||
|
||||
visitText(cmd: RenderTextCmd, context: any): any {
|
||||
var text = this.factory.createText(cmd.value);
|
||||
this._addChild(text, cmd.ngContentIndex);
|
||||
if (cmd.isBound) {
|
||||
this.boundTextNodes.push(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
visitNgContent(cmd: RenderNgContentCmd, context: any): any {
|
||||
if (isPresent(this.parentComponent)) {
|
||||
var projectedNodes = this.parentComponent.project();
|
||||
for (var i = 0; i < projectedNodes.length; i++) {
|
||||
var node = projectedNodes[i];
|
||||
this._addChild(node, cmd.ngContentIndex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
visitBeginElement(cmd: RenderBeginElementCmd, context: any): any {
|
||||
this.parentStack.push(this._beginElement(cmd));
|
||||
return null;
|
||||
}
|
||||
visitEndElement(context: any): any {
|
||||
this._endElement();
|
||||
return null;
|
||||
}
|
||||
visitBeginComponent(cmd: RenderBeginComponentCmd, context: any): any {
|
||||
var el = this._beginElement(cmd);
|
||||
var root = el;
|
||||
if (cmd.nativeShadow) {
|
||||
root = this.factory.createShadowRoot(el);
|
||||
this.nativeShadowRoots.push(root);
|
||||
}
|
||||
this.parentStack.push(new Component(el, root, cmd, this.factory));
|
||||
return null;
|
||||
}
|
||||
visitEndComponent(context: any): any {
|
||||
var c = <Component<N>>this.parent;
|
||||
var template = this.factory.resolveComponentTemplate(c.cmd.templateId);
|
||||
this._visitChildTemplate(template, c, c.shadowRoot);
|
||||
this._endElement();
|
||||
return null;
|
||||
}
|
||||
visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: any): any {
|
||||
var el = this.factory.createTemplateAnchor(cmd.attrNameAndValues);
|
||||
this._addChild(el, cmd.ngContentIndex);
|
||||
this.boundElements.push(el);
|
||||
if (cmd.isMerged) {
|
||||
this._visitChildTemplate(cmd.children, this.parentComponent, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _beginElement(cmd: RenderBeginElementCmd): N {
|
||||
var el: N;
|
||||
if (isPresent(this.inplaceElement)) {
|
||||
el = this.inplaceElement;
|
||||
this.inplaceElement = null;
|
||||
this.factory.mergeElement(el, cmd.attrNameAndValues);
|
||||
this.fragmentRootNodes.push(el);
|
||||
} else {
|
||||
el = this.factory.createElement(cmd.name, cmd.attrNameAndValues);
|
||||
this._addChild(el, cmd.ngContentIndex);
|
||||
}
|
||||
if (cmd.isBound) {
|
||||
this.boundElements.push(el);
|
||||
for (var i = 0; i < cmd.eventTargetAndNames.length; i += 2) {
|
||||
var target = cmd.eventTargetAndNames[i];
|
||||
var eventName = cmd.eventTargetAndNames[i + 1];
|
||||
this.eventData.push([this.boundElements.length - 1, target, eventName]);
|
||||
}
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
private _endElement() { this.parentStack.pop(); }
|
||||
|
||||
private _visitChildTemplate(cmds: RenderTemplateCmd[], parent: Component<N>, rootNodesParent: N) {
|
||||
visitAll(new RenderViewBuilder(parent, rootNodesParent, null, this.allBuilders, this.factory),
|
||||
cmds);
|
||||
}
|
||||
|
||||
private _addChild(node: N, ngContentIndex: number) {
|
||||
var parent = this.parent;
|
||||
if (isPresent(parent)) {
|
||||
if (parent instanceof Component) {
|
||||
parent.addContentNode(ngContentIndex, node);
|
||||
} else {
|
||||
this.factory.appendChild(<N>parent, node);
|
||||
}
|
||||
} else {
|
||||
this.fragmentRootNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Component<N> {
|
||||
private contentNodesByNgContentIndex: N[][] = [];
|
||||
private projectingNgContentIndex: number = 0;
|
||||
|
||||
constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd,
|
||||
public factory: NodeFactory<N>) {}
|
||||
addContentNode(ngContentIndex: number, node: N) {
|
||||
if (isBlank(ngContentIndex)) {
|
||||
if (this.cmd.nativeShadow) {
|
||||
this.factory.appendChild(this.hostElement, node);
|
||||
}
|
||||
} else {
|
||||
while (this.contentNodesByNgContentIndex.length <= ngContentIndex) {
|
||||
this.contentNodesByNgContentIndex.push([]);
|
||||
}
|
||||
this.contentNodesByNgContentIndex[ngContentIndex].push(node);
|
||||
}
|
||||
}
|
||||
project(): N[] {
|
||||
var ngContentIndex = this.projectingNgContentIndex++;
|
||||
return ngContentIndex < this.contentNodesByNgContentIndex.length ?
|
||||
this.contentNodesByNgContentIndex[ngContentIndex] :
|
||||
[];
|
||||
}
|
||||
}
|
||||
|
||||
function addAll(source: any[], target: any[]) {
|
||||
for (var i = 0; i < source.length; i++) {
|
||||
target.push(source[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function visitAll(visitor: RenderCommandVisitor, fragmentCmds: RenderTemplateCmd[]) {
|
||||
for (var i = 0; i < fragmentCmds.length; i++) {
|
||||
fragmentCmds[i].visit(visitor, null);
|
||||
}
|
||||
}
|
|
@ -119,7 +119,8 @@ export function stringifyElement(el): string {
|
|||
result += '>';
|
||||
|
||||
// Children
|
||||
var children = DOM.childNodes(DOM.templateAwareRoot(el));
|
||||
var childrenRoot = DOM.templateAwareRoot(el);
|
||||
var children = isPresent(childrenRoot) ? DOM.childNodes(childrenRoot) : [];
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
result += stringifyElement(children[j]);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,25 @@ export function main() {
|
|||
|
||||
});
|
||||
|
||||
it('should be able to create text nodes and use them with the other APIs', () => {
|
||||
var t = DOM.createTextNode('hello');
|
||||
expect(DOM.isTextNode(t)).toBe(true);
|
||||
var d = DOM.createElement('div');
|
||||
DOM.appendChild(d, t);
|
||||
expect(DOM.getInnerHTML(d)).toEqual('hello');
|
||||
});
|
||||
|
||||
it('should set className via the class attribute', () => {
|
||||
var d = DOM.createElement('div');
|
||||
DOM.setAttribute(d, 'class', 'class1');
|
||||
expect(d.className).toEqual('class1');
|
||||
});
|
||||
|
||||
it('should allow to remove nodes without parents', () => {
|
||||
var d = DOM.createElement('div');
|
||||
expect(() => DOM.remove(d)).not.toThrow();
|
||||
});
|
||||
|
||||
if (DOM.supportsDOMEvents()) {
|
||||
describe('getBaseHref', () => {
|
||||
beforeEach(() => DOM.resetBaseElement());
|
||||
|
|
|
@ -0,0 +1,526 @@
|
|||
import {
|
||||
AsyncTestCompleter,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit,
|
||||
stringifyElement
|
||||
} from 'angular2/test_lib';
|
||||
|
||||
import {isPresent} from 'angular2/src/core/facade/lang';
|
||||
import {MapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import * as appCmds from 'angular2/src/core/compiler/template_commands';
|
||||
import {createRenderView, NodeFactory} from 'angular2/src/core/render/view_factory';
|
||||
import {RenderTemplateCmd, RenderBeginElementCmd} from 'angular2/src/core/render/api';
|
||||
import {SpyRenderEventDispatcher} from '../spies';
|
||||
import {DOM} from 'angular2/src/core/dom/dom_adapter';
|
||||
|
||||
function beginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
||||
isBound: boolean, ngContentIndex: number): RenderBeginElementCmd {
|
||||
return appCmds.beginElement(name, attrNameAndValues, eventTargetAndNames, [], [], isBound,
|
||||
ngContentIndex)
|
||||
}
|
||||
|
||||
function endElement() {
|
||||
return appCmds.endElement();
|
||||
}
|
||||
|
||||
function text(value: string, isBound: boolean, ngContentIndex: number) {
|
||||
return appCmds.text(value, isBound, ngContentIndex);
|
||||
}
|
||||
|
||||
function embeddedTemplate(attrNameAndValues: string[], isMerged: boolean, ngContentIndex: number,
|
||||
children: any[]) {
|
||||
return appCmds.embeddedTemplate(attrNameAndValues, [], [], isMerged, ngContentIndex, null,
|
||||
children);
|
||||
}
|
||||
|
||||
function beginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
||||
nativeShadow: boolean, ngContentIndex: number, templateId: number) {
|
||||
return appCmds.beginComponent(name, attrNameAndValues, eventTargetAndNames, [], [], nativeShadow,
|
||||
ngContentIndex, new appCmds.CompiledTemplate(templateId, null));
|
||||
}
|
||||
|
||||
function endComponent() {
|
||||
return appCmds.endComponent();
|
||||
}
|
||||
|
||||
function ngContent(ngContentIndex: number) {
|
||||
return appCmds.ngContent(ngContentIndex);
|
||||
}
|
||||
|
||||
export function main() {
|
||||
describe('createRenderView', () => {
|
||||
var nodeFactory: DomNodeFactory;
|
||||
var eventDispatcher: SpyRenderEventDispatcher;
|
||||
var componentTemplates: Map<number, RenderTemplateCmd[]> = new Map();
|
||||
beforeEach(() => {
|
||||
nodeFactory = new DomNodeFactory(componentTemplates);
|
||||
eventDispatcher = new SpyRenderEventDispatcher();
|
||||
});
|
||||
|
||||
describe('primitives', () => {
|
||||
|
||||
it('should create elements with attributes', () => {
|
||||
var view = createRenderView(
|
||||
[beginElement('div', ['attr1', 'value1'], [], false, null), endElement()], null,
|
||||
nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes)).toEqual('<div attr1="value1"></div>');
|
||||
});
|
||||
|
||||
it('should create host elements with attributes', () => {
|
||||
componentTemplates.set(0, []);
|
||||
var view = createRenderView(
|
||||
[beginComponent('a-comp', ['attr1', 'value1'], [], false, null, 0), endElement()], null,
|
||||
nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes))
|
||||
.toEqual('<a-comp attr1="value1"></a-comp>');
|
||||
});
|
||||
|
||||
it('should create embedded templates with attributes', () => {
|
||||
componentTemplates.set(0, []);
|
||||
var view = createRenderView([embeddedTemplate(['attr1', 'value1'], false, null, [])], null,
|
||||
nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes))
|
||||
.toEqual('<template attr1="value1"></template>');
|
||||
});
|
||||
|
||||
it('should store bound elements', () => {
|
||||
componentTemplates.set(0, []);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('div', ['id', '1'], [], false, null),
|
||||
endElement(),
|
||||
beginElement('span', ['id', '2'], [], true, null),
|
||||
endElement(),
|
||||
beginComponent('a-comp', ['id', '3'], [], false, null, 0),
|
||||
endElement(),
|
||||
embeddedTemplate(['id', '4'], false, null, [])
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(mapAttrs(view.boundElements, 'id')).toEqual(['2', '3', '4']);
|
||||
});
|
||||
|
||||
it('should use the inplace element for the first create element', () => {
|
||||
var el = DOM.createElement('span');
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('div', ['attr1', 'value1'], [], false, null),
|
||||
endElement(),
|
||||
beginElement('div', [], [], false, null),
|
||||
endElement()
|
||||
],
|
||||
el, nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes))
|
||||
.toEqual('<span attr1="value1"></span><div></div>');
|
||||
});
|
||||
|
||||
it('should create text nodes', () => {
|
||||
var view = createRenderView([text('someText', false, null)], null, nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes)).toEqual('someText');
|
||||
});
|
||||
|
||||
it('should store bound text nodes', () => {
|
||||
var view =
|
||||
createRenderView([text('1', false, null), text('2', true, null)], null, nodeFactory);
|
||||
expect(stringifyElement(view.boundTextNodes[0])).toEqual('2');
|
||||
});
|
||||
|
||||
it('should register element event listeners', () => {
|
||||
componentTemplates.set(0, []);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('div', [], [null, 'click'], true, null),
|
||||
endElement(),
|
||||
beginComponent('a-comp', [], [null, 'click'], false, null, 0),
|
||||
endElement(),
|
||||
],
|
||||
null, nodeFactory);
|
||||
view.setEventDispatcher(<any>eventDispatcher);
|
||||
var event = {};
|
||||
nodeFactory.triggerLocalEvent(view.boundElements[0], 'click', event);
|
||||
nodeFactory.triggerLocalEvent(view.boundElements[1], 'click', event);
|
||||
expect(eventDispatcher.spy('dispatchRenderEvent'))
|
||||
.toHaveBeenCalledWith(0, 'click', MapWrapper.createFromStringMap({'$event': event}));
|
||||
expect(eventDispatcher.spy('dispatchRenderEvent'))
|
||||
.toHaveBeenCalledWith(1, 'click', MapWrapper.createFromStringMap({'$event': event}));
|
||||
});
|
||||
|
||||
it('should register element global event listeners', () => {
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('div', [], ['window', 'scroll'], true, null),
|
||||
endElement(),
|
||||
beginComponent('a-comp', [], ['window', 'scroll'], false, null, 0),
|
||||
endElement(),
|
||||
],
|
||||
null, nodeFactory);
|
||||
view.hydrate();
|
||||
view.setEventDispatcher(<any>eventDispatcher);
|
||||
var event = {};
|
||||
nodeFactory.triggerGlobalEvent('window', 'scroll', event);
|
||||
expect(eventDispatcher.spy('dispatchRenderEvent'))
|
||||
.toHaveBeenCalledWith(0, 'window:scroll',
|
||||
MapWrapper.createFromStringMap({'$event': event}));
|
||||
expect(eventDispatcher.spy('dispatchRenderEvent'))
|
||||
.toHaveBeenCalledWith(1, 'window:scroll',
|
||||
MapWrapper.createFromStringMap({'$event': event}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('nested nodes', () => {
|
||||
it('should create nested node', () => {
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('a', [], [], false, null),
|
||||
beginElement('b', [], [], false, null),
|
||||
text('someText', false, null),
|
||||
endElement(),
|
||||
endElement(),
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes)).toEqual('<a><b>someText</b></a>');
|
||||
});
|
||||
|
||||
it('should store bound elements in depth first order', () => {
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('a', ['id', '1'], [], false, null),
|
||||
endElement(),
|
||||
beginElement('a', ['id', '2'], [], true, null),
|
||||
beginElement('a', ['id', '3'], [], false, null),
|
||||
endElement(),
|
||||
beginElement('a', ['id', '4'], [], true, null),
|
||||
endElement(),
|
||||
endElement(),
|
||||
beginElement('a', ['id', '5'], [], false, null),
|
||||
endElement(),
|
||||
beginElement('a', ['id', '6'], [], true, null),
|
||||
endElement(),
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(mapAttrs(view.boundElements, 'id')).toEqual(['2', '4', '6']);
|
||||
});
|
||||
|
||||
it('should store bound text nodes in depth first order', () => {
|
||||
var view = createRenderView(
|
||||
[
|
||||
text('1', false, null),
|
||||
text('2', true, null),
|
||||
beginElement('a', [], [], false, null),
|
||||
text('3', false, null),
|
||||
text('4', true, null),
|
||||
endElement(),
|
||||
text('5', false, null),
|
||||
text('6', true, null),
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(mapText(view.boundTextNodes)).toEqual(['2', '4', '6']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('merged embedded templates', () => {
|
||||
it('should create separate fragments', () => {
|
||||
var view = createRenderView(
|
||||
[embeddedTemplate(['attr1', 'value1'], true, null, [text('someText', false, null)])],
|
||||
null, nodeFactory);
|
||||
expect(view.fragments.length).toBe(2);
|
||||
expect(stringifyFragment(view.fragments[1].nodes)).toEqual('someText');
|
||||
});
|
||||
|
||||
it('should store bound elements after the bound elements of earlier fragments', () => {
|
||||
var view =
|
||||
createRenderView(
|
||||
[
|
||||
beginElement('a', ['id', '1.1'], [], true, null),
|
||||
endElement(),
|
||||
embeddedTemplate(['id', '1.2'], true, null,
|
||||
[
|
||||
embeddedTemplate(['id', '2.1'], true, null,
|
||||
[
|
||||
beginElement('a', ['id', '3.1'],
|
||||
[], true, null),
|
||||
endElement()
|
||||
]),
|
||||
beginElement('a', ['id', '2.2'], [], true, null),
|
||||
endElement(),
|
||||
]),
|
||||
beginElement('a', ['id', '1.3'], [], true, null),
|
||||
endElement(),
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(mapAttrs(view.boundElements, 'id'))
|
||||
.toEqual(['1.1', '1.2', '1.3', '2.1', '2.2', '3.1']);
|
||||
});
|
||||
|
||||
it('should store bound text nodes after the bound text nodes of earlier fragments', () => {
|
||||
var view =
|
||||
createRenderView(
|
||||
[
|
||||
text('1.1', true, null),
|
||||
embeddedTemplate(['id', '1.2'], true, null,
|
||||
[
|
||||
text('2.1', true, null),
|
||||
embeddedTemplate(['id', '2.1'], true, null,
|
||||
[
|
||||
text('3.1', true, null),
|
||||
]),
|
||||
text('2.2', true, null),
|
||||
]),
|
||||
text('1.2', true, null),
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(mapText(view.boundTextNodes)).toEqual(['1.1', '1.2', '2.1', '2.2', '3.1']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('non merged embedded templates', () => {
|
||||
it('should only create the anchor element', () => {
|
||||
var view = createRenderView(
|
||||
[
|
||||
embeddedTemplate(['id', '1.1'], false, null,
|
||||
[
|
||||
text('someText', true, null),
|
||||
beginElement('a', ['id', '2.1'], [], true, null),
|
||||
endElement()
|
||||
])
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(view.fragments.length).toBe(1);
|
||||
expect(stringifyFragment(view.fragments[0].nodes))
|
||||
.toEqual('<template id="1.1"></template>');
|
||||
expect(view.boundTextNodes.length).toBe(0);
|
||||
expect(mapAttrs(view.boundElements, 'id')).toEqual(['1.1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('components', () => {
|
||||
it('should store the component template in the same fragment', () => {
|
||||
componentTemplates.set(0, [
|
||||
text('hello', false, null),
|
||||
]);
|
||||
var view = createRenderView(
|
||||
[beginComponent('my-comp', [], [], false, null, 0), endComponent()], null, nodeFactory);
|
||||
expect(view.fragments.length).toBe(1);
|
||||
expect(stringifyFragment(view.fragments[0].nodes)).toEqual('<my-comp>hello</my-comp>');
|
||||
});
|
||||
|
||||
it('should use native shadow DOM', () => {
|
||||
componentTemplates.set(0, [
|
||||
text('hello', false, null),
|
||||
]);
|
||||
var view = createRenderView(
|
||||
[beginComponent('my-comp', [], [], true, null, 0), endComponent()], null, nodeFactory);
|
||||
expect(view.fragments.length).toBe(1);
|
||||
expect(stringifyFragment(view.fragments[0].nodes))
|
||||
.toEqual('<my-comp><shadow-root>hello</shadow-root></my-comp>');
|
||||
});
|
||||
|
||||
it('should store bound elements after the bound elements of the main template', () => {
|
||||
componentTemplates.set(0, [
|
||||
beginComponent('b-comp', ['id', '2.1'], [], false, null, 1),
|
||||
endComponent(),
|
||||
beginComponent('b-comp', ['id', '2.2'], [], false, null, 1),
|
||||
endComponent(),
|
||||
]);
|
||||
componentTemplates.set(1, [beginElement('a', ['id', '3.1'], [], true, null), endElement()]);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginElement('a', ['id', '1.1'], [], true, null),
|
||||
endElement(),
|
||||
beginComponent('a-comp', ['id', '1.2'], [], false, null, 0),
|
||||
beginElement('a', ['id', '1.3'], [], true, null),
|
||||
endElement(),
|
||||
endComponent(),
|
||||
beginElement('a', ['id', '1.4'], [], true, null),
|
||||
endElement(),
|
||||
],
|
||||
null, nodeFactory);
|
||||
|
||||
expect(mapAttrs(view.boundElements, 'id'))
|
||||
.toEqual(['1.1', '1.2', '1.3', '1.4', '2.1', '2.2', '3.1', '3.1']);
|
||||
});
|
||||
|
||||
it('should store bound text nodes after the bound text nodes of the main template', () => {
|
||||
componentTemplates.set(0, [
|
||||
text('2.1', true, null),
|
||||
beginComponent('b-comp', [], [], false, null, 1),
|
||||
endComponent(),
|
||||
beginComponent('b-comp', [], [], false, null, 1),
|
||||
endComponent(),
|
||||
text('2.2', true, null),
|
||||
]);
|
||||
componentTemplates.set(1, [
|
||||
text('3.1', true, null),
|
||||
]);
|
||||
var view = createRenderView(
|
||||
[
|
||||
text('1.1', true, null),
|
||||
beginComponent('a-comp', [], [], false, null, 0),
|
||||
text('1.2', true, null),
|
||||
endComponent(),
|
||||
text('1.3', true, null),
|
||||
],
|
||||
null, nodeFactory);
|
||||
|
||||
expect(mapText(view.boundTextNodes))
|
||||
.toEqual(['1.1', '1.2', '1.3', '2.1', '2.2', '3.1', '3.1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('content projection', () => {
|
||||
it('should remove non projected nodes', () => {
|
||||
componentTemplates.set(0, []);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginComponent('my-comp', [], [], false, null, 0),
|
||||
text('hello', false, null),
|
||||
endComponent()
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes)).toEqual('<my-comp></my-comp>');
|
||||
});
|
||||
|
||||
it('should keep non projected nodes in the light dom when using native shadow dom', () => {
|
||||
componentTemplates.set(0, []);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginComponent('my-comp', [], [], true, null, 0),
|
||||
text('hello', false, null),
|
||||
endComponent()
|
||||
],
|
||||
null, nodeFactory);
|
||||
var rootEl = view.fragments[0].nodes[0];
|
||||
expect(stringifyElement(rootEl))
|
||||
.toEqual('<my-comp><shadow-root></shadow-root>hello</my-comp>');
|
||||
});
|
||||
|
||||
it('should project commands based on their ngContentIndex', () => {
|
||||
componentTemplates.set(0, [
|
||||
text('(', false, null),
|
||||
ngContent(null),
|
||||
text(',', false, null),
|
||||
ngContent(null),
|
||||
text(')', false, null)
|
||||
]);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginComponent('my-comp', [], [], false, null, 0),
|
||||
text('2', false, 1),
|
||||
text('1', false, 0),
|
||||
endComponent()
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes)).toEqual('<my-comp>(1,2)</my-comp>');
|
||||
});
|
||||
|
||||
it('should reproject nodes over multiple ng-content commands', () => {
|
||||
componentTemplates.set(
|
||||
0, [beginComponent('b-comp', [], [], false, null, 1), ngContent(0), endComponent()]);
|
||||
componentTemplates.set(1,
|
||||
[text('(', false, null), ngContent(null), text(')', false, null)]);
|
||||
var view = createRenderView(
|
||||
[
|
||||
beginComponent('a-comp', [], [], false, null, 0),
|
||||
text('hello', false, 0),
|
||||
endComponent()
|
||||
],
|
||||
null, nodeFactory);
|
||||
expect(stringifyFragment(view.fragments[0].nodes))
|
||||
.toEqual('<a-comp><b-comp>(hello)</b-comp></a-comp>');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class DomNodeFactory implements NodeFactory<Node> {
|
||||
private _globalEventListeners: GlobalEventListener[] = [];
|
||||
private _localEventListeners: LocalEventListener[] = [];
|
||||
|
||||
constructor(private _components: Map<number, RenderTemplateCmd[]>) {}
|
||||
|
||||
triggerLocalEvent(el: Element, eventName: string, event: any) {
|
||||
this._localEventListeners.forEach(listener => {
|
||||
if (listener.eventName == eventName) {
|
||||
listener.callback(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
triggerGlobalEvent(target: string, eventName: string, event: any) {
|
||||
this._globalEventListeners.forEach(listener => {
|
||||
if (listener.eventName == eventName && listener.target == target) {
|
||||
listener.callback(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveComponentTemplate(templateId: number): RenderTemplateCmd[] {
|
||||
return this._components.get(templateId);
|
||||
}
|
||||
createTemplateAnchor(attrNameAndValues: string[]): Node {
|
||||
var el = DOM.createElement('template');
|
||||
this._setAttributes(el, attrNameAndValues);
|
||||
return el;
|
||||
}
|
||||
createElement(name: string, attrNameAndValues: string[]): Node {
|
||||
var el = DOM.createElement(name);
|
||||
this._setAttributes(el, attrNameAndValues);
|
||||
return el;
|
||||
}
|
||||
mergeElement(existing: Node, attrNameAndValues: string[]) {
|
||||
DOM.clearNodes(existing);
|
||||
this._setAttributes(existing, attrNameAndValues);
|
||||
}
|
||||
private _setAttributes(el: Node, attrNameAndValues: string[]) {
|
||||
for (var attrIdx = 0; attrIdx < attrNameAndValues.length; attrIdx += 2) {
|
||||
DOM.setAttribute(el, attrNameAndValues[attrIdx], attrNameAndValues[attrIdx + 1]);
|
||||
}
|
||||
}
|
||||
createShadowRoot(host: Node): Node {
|
||||
var root = DOM.createElement('shadow-root');
|
||||
DOM.appendChild(host, root);
|
||||
return root;
|
||||
}
|
||||
createText(value: string): Node { return DOM.createTextNode(isPresent(value) ? value : ''); }
|
||||
appendChild(parent: Node, child: Node) { DOM.appendChild(parent, child); }
|
||||
on(element: Node, eventName: string, callback: Function) {
|
||||
this._localEventListeners.push(new LocalEventListener(element, eventName, callback));
|
||||
}
|
||||
globalOn(target: string, eventName: string, callback: Function): Function {
|
||||
var listener = new GlobalEventListener(target, eventName, callback);
|
||||
this._globalEventListeners.push(listener);
|
||||
return () => {
|
||||
var index = this._globalEventListeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this._globalEventListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LocalEventListener {
|
||||
constructor(public element: Node, public eventName: string, public callback: Function) {}
|
||||
}
|
||||
|
||||
class GlobalEventListener {
|
||||
constructor(public target: string, public eventName: string, public callback: Function) {}
|
||||
}
|
||||
|
||||
function stringifyFragment(nodes: Node[]) {
|
||||
return nodes.map(stringifyElement).join('');
|
||||
}
|
||||
|
||||
function mapAttrs(nodes: Node[], attrName): string[] {
|
||||
return nodes.map(node => DOM.getAttribute(node, attrName));
|
||||
}
|
||||
|
||||
function mapText(nodes: Node[]): string[] {
|
||||
return nodes.map(node => DOM.getText(node));
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
AsyncTestCompleter,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
it,
|
||||
xit
|
||||
} from 'angular2/test_lib';
|
||||
|
||||
import {DefaultRenderView} from 'angular2/src/core/render/view';
|
||||
|
||||
export function main() {
|
||||
describe('DefaultRenderView', () => {
|
||||
describe('hydrate', () => {
|
||||
it('should register global event listeners', () => {
|
||||
var addCount = 0;
|
||||
var adder = () => { addCount++ };
|
||||
var view = new DefaultRenderView<Node>([], [], [], [], [adder]);
|
||||
view.hydrate();
|
||||
expect(addCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dehydrate', () => {
|
||||
it('should deregister global event listeners', () => {
|
||||
var removeCount = 0;
|
||||
var adder = () => () => { removeCount++ };
|
||||
var view = new DefaultRenderView<Node>([], [], [], [], [adder]);
|
||||
view.hydrate();
|
||||
view.dehydrate();
|
||||
expect(removeCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -110,3 +110,8 @@ class SpyDomAdapter extends SpyObject implements DomAdapter {
|
|||
class SpyXHR extends SpyObject implements XHR {
|
||||
noSuchMethod(m) => super.noSuchMethod(m);
|
||||
}
|
||||
|
||||
@proxy
|
||||
class SpyRenderEventDispatcher extends SpyObject implements RenderEventDispatcher {
|
||||
noSuchMethod(m) => super.noSuchMethod(m);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
DynamicChangeDetector
|
||||
} from 'angular2/src/core/change_detection/change_detection';
|
||||
|
||||
import {RenderCompiler, Renderer} from 'angular2/src/core/render/api';
|
||||
import {RenderCompiler, Renderer, RenderEventDispatcher} from 'angular2/src/core/render/api';
|
||||
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
|
||||
|
||||
import {AppView} from 'angular2/src/core/compiler/view';
|
||||
|
@ -92,4 +92,14 @@ export class SpyDomAdapter extends SpyObject {
|
|||
|
||||
export class SpyXHR extends SpyObject {
|
||||
constructor() { super(XHR); }
|
||||
}
|
||||
}
|
||||
|
||||
export class SpyRenderEventDispatcher extends SpyObject {
|
||||
constructor() {
|
||||
// Note: RenderEventDispatcher is an interface,
|
||||
// so we can't pass it to super() and have to register
|
||||
// the spy methods on our own.
|
||||
super();
|
||||
this.spy('dispatchRenderEvent');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue