feat(render): add generic view factory based on the template commands

Part of #3605
Closes #4367
This commit is contained in:
Tobias Bosch
2015-09-25 09:43:21 -07:00
parent 0ed6fc4f6b
commit 1cf45757cd
11 changed files with 906 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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