X Tutup
Skip to content

Commit 1cf4575

Browse files
committed
feat(render): add generic view factory based on the template commands
Part of angular#3605 Closes angular#4367
1 parent 0ed6fc4 commit 1cf4575

File tree

11 files changed

+906
-8
lines changed

11 files changed

+906
-8
lines changed

modules/angular2/src/core/dom/browser_adapter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
158158
removeChild(el, node) { el.removeChild(node); }
159159
replaceChild(el: Node, newChild, oldChild) { el.replaceChild(newChild, oldChild); }
160160
remove(node): Node {
161-
node.parentNode.removeChild(node);
161+
if (node.parentNode) {
162+
node.parentNode.removeChild(node);
163+
}
162164
return node;
163165
}
164166
insertBefore(el, node) { el.parentNode.insertBefore(node, el); }

modules/angular2/src/core/dom/html_adapter.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,7 @@ class Html5LibDomAdapter implements DomAdapter {
221221
return new Element.tag(tagName);
222222
}
223223

224-
createTextNode(String text, [doc]) {
225-
throw 'not implemented';
226-
}
224+
createTextNode(String text, [doc]) => new Text(text);
227225

228226
createScriptTag(String attrName, String attrValue, [doc]) {
229227
throw 'not implemented';

modules/angular2/src/core/dom/parse5_adapter.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,11 @@ export class Parse5DomAdapter extends DomAdapter {
276276
createElement(tagName): HTMLElement {
277277
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []);
278278
}
279-
createTextNode(text: string): Text { throw _notImplemented('createTextNode'); }
279+
createTextNode(text: string): Text {
280+
var t = <any>this.createComment(text);
281+
t.type = 'text';
282+
return t;
283+
}
280284
createScriptTag(attrName: string, attrValue: string): HTMLElement {
281285
return treeAdapter.createElement("script", 'http://www.w3.org/1999/xhtml',
282286
[{name: attrName, value: attrValue}]);
@@ -424,6 +428,9 @@ export class Parse5DomAdapter extends DomAdapter {
424428
setAttribute(element, attribute: string, value: string) {
425429
if (attribute) {
426430
element.attribs[attribute] = value;
431+
if (attribute === 'class') {
432+
element.className = value;
433+
}
427434
}
428435
}
429436
removeAttribute(element, attribute: string) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {BaseException} from 'angular2/src/core/facade/exceptions';
2+
import {ListWrapper, MapWrapper, Map, StringMapWrapper} from 'angular2/src/core/facade/collection';
3+
import {isPresent, isBlank, stringify} from 'angular2/src/core/facade/lang';
4+
5+
import {
6+
RenderViewRef,
7+
RenderEventDispatcher,
8+
RenderTemplateCmd,
9+
RenderProtoViewRef,
10+
RenderFragmentRef
11+
} from './api';
12+
13+
export class DefaultProtoViewRef extends RenderProtoViewRef {
14+
constructor(public cmds: RenderTemplateCmd[]) { super(); }
15+
}
16+
17+
export class DefaultRenderFragmentRef<N> extends RenderFragmentRef {
18+
constructor(public nodes: N[]) { super(); }
19+
}
20+
21+
export class DefaultRenderView<N> extends RenderViewRef {
22+
hydrated: boolean = false;
23+
eventDispatcher: RenderEventDispatcher = null;
24+
globalEventRemovers: Function[] = null;
25+
26+
constructor(public fragments: DefaultRenderFragmentRef<N>[], public boundTextNodes: N[],
27+
public boundElements: N[], public nativeShadowRoots: N[],
28+
public globalEventAdders: Function[]) {
29+
super();
30+
}
31+
32+
hydrate() {
33+
if (this.hydrated) throw new BaseException('The view is already hydrated.');
34+
this.hydrated = true;
35+
this.globalEventRemovers = ListWrapper.createFixedSize(this.globalEventAdders.length);
36+
for (var i = 0; i < this.globalEventAdders.length; i++) {
37+
this.globalEventRemovers[i] = this.globalEventAdders[i]();
38+
}
39+
}
40+
41+
dehydrate() {
42+
if (!this.hydrated) throw new BaseException('The view is already dehydrated.');
43+
for (var i = 0; i < this.globalEventRemovers.length; i++) {
44+
this.globalEventRemovers[i]();
45+
}
46+
this.globalEventRemovers = null;
47+
this.hydrated = false;
48+
}
49+
50+
setEventDispatcher(dispatcher: RenderEventDispatcher) { this.eventDispatcher = dispatcher; }
51+
52+
dispatchRenderEvent(boundElementIndex: number, eventName: string, event: any): boolean {
53+
var allowDefaultBehavior = true;
54+
if (isPresent(this.eventDispatcher)) {
55+
var locals = new Map();
56+
locals.set('$event', event);
57+
allowDefaultBehavior =
58+
this.eventDispatcher.dispatchRenderEvent(boundElementIndex, eventName, locals);
59+
}
60+
return allowDefaultBehavior;
61+
}
62+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import {isBlank, isPresent} from 'angular2/src/core/facade/lang';
2+
import {
3+
RenderEventDispatcher,
4+
RenderTemplateCmd,
5+
RenderCommandVisitor,
6+
RenderBeginElementCmd,
7+
RenderBeginComponentCmd,
8+
RenderNgContentCmd,
9+
RenderTextCmd,
10+
RenderEmbeddedTemplateCmd
11+
} from './api';
12+
import {DefaultRenderView, DefaultRenderFragmentRef} from './view';
13+
14+
export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceElement: any,
15+
nodeFactory: NodeFactory<any>): DefaultRenderView<any> {
16+
var builders: RenderViewBuilder<any>[] = [];
17+
visitAll(new RenderViewBuilder<any>(null, null, inplaceElement, builders, nodeFactory),
18+
fragmentCmds);
19+
var boundElements: any[] = [];
20+
var boundTextNodes: any[] = [];
21+
var nativeShadowRoots: any[] = [];
22+
var fragments: DefaultRenderFragmentRef<any>[] = [];
23+
var viewElementOffset = 0;
24+
var view: DefaultRenderView<any>;
25+
var eventDispatcher = (boundElementIndex: number, eventName: string, event: any) =>
26+
view.dispatchRenderEvent(boundElementIndex, eventName, event);
27+
var globalEventAdders: Function[] = [];
28+
29+
for (var i = 0; i < builders.length; i++) {
30+
var builder = builders[i];
31+
addAll(builder.boundElements, boundElements);
32+
addAll(builder.boundTextNodes, boundTextNodes);
33+
addAll(builder.nativeShadowRoots, nativeShadowRoots);
34+
if (isBlank(builder.rootNodesParent)) {
35+
fragments.push(new DefaultRenderFragmentRef<any>(builder.fragmentRootNodes));
36+
}
37+
for (var j = 0; j < builder.eventData.length; j++) {
38+
var eventData = builder.eventData[j];
39+
var boundElementIndex = eventData[0] + viewElementOffset;
40+
var target = eventData[1];
41+
var eventName = eventData[2];
42+
if (isPresent(target)) {
43+
var handler =
44+
createEventHandler(boundElementIndex, `${target}:${eventName}`, eventDispatcher);
45+
globalEventAdders.push(createGlobalEventAdder(target, eventName, handler, nodeFactory));
46+
} else {
47+
var handler = createEventHandler(boundElementIndex, eventName, eventDispatcher);
48+
nodeFactory.on(boundElements[boundElementIndex], eventName, handler);
49+
}
50+
}
51+
viewElementOffset += builder.boundElements.length;
52+
}
53+
view = new DefaultRenderView<any>(fragments, boundTextNodes, boundElements, nativeShadowRoots,
54+
globalEventAdders);
55+
return view;
56+
}
57+
58+
function createEventHandler(boundElementIndex: number, eventName: string,
59+
eventDispatcher: Function): Function {
60+
return ($event) => eventDispatcher(boundElementIndex, eventName, $event);
61+
}
62+
63+
function createGlobalEventAdder(target: string, eventName: string, eventHandler: Function,
64+
nodeFactory: NodeFactory<any>): Function {
65+
return () => nodeFactory.globalOn(target, eventName, eventHandler);
66+
}
67+
68+
export interface NodeFactory<N> {
69+
resolveComponentTemplate(templateId: number): RenderTemplateCmd[];
70+
createTemplateAnchor(attrNameAndValues: string[]): N;
71+
createElement(name: string, attrNameAndValues: string[]): N;
72+
mergeElement(existing: N, attrNameAndValues: string[]);
73+
createShadowRoot(host: N): N;
74+
createText(value: string): N;
75+
appendChild(parent: N, child: N);
76+
on(element: N, eventName: string, callback: Function);
77+
globalOn(target: string, eventName: string, callback: Function): Function;
78+
}
79+
80+
class RenderViewBuilder<N> implements RenderCommandVisitor {
81+
parentStack: Array<N | Component<N>>;
82+
boundTextNodes: N[] = [];
83+
boundElements: N[] = [];
84+
eventData: any[][] = [];
85+
86+
fragmentRootNodes: N[] = [];
87+
nativeShadowRoots: N[] = [];
88+
89+
constructor(public parentComponent: Component<N>, public rootNodesParent: N,
90+
public inplaceElement: N, public allBuilders: RenderViewBuilder<N>[],
91+
public factory: NodeFactory<N>) {
92+
this.parentStack = [rootNodesParent];
93+
allBuilders.push(this);
94+
}
95+
96+
get parent(): N | Component<N> { return this.parentStack[this.parentStack.length - 1]; }
97+
98+
visitText(cmd: RenderTextCmd, context: any): any {
99+
var text = this.factory.createText(cmd.value);
100+
this._addChild(text, cmd.ngContentIndex);
101+
if (cmd.isBound) {
102+
this.boundTextNodes.push(text);
103+
}
104+
return null;
105+
}
106+
visitNgContent(cmd: RenderNgContentCmd, context: any): any {
107+
if (isPresent(this.parentComponent)) {
108+
var projectedNodes = this.parentComponent.project();
109+
for (var i = 0; i < projectedNodes.length; i++) {
110+
var node = projectedNodes[i];
111+
this._addChild(node, cmd.ngContentIndex);
112+
}
113+
}
114+
return null;
115+
}
116+
visitBeginElement(cmd: RenderBeginElementCmd, context: any): any {
117+
this.parentStack.push(this._beginElement(cmd));
118+
return null;
119+
}
120+
visitEndElement(context: any): any {
121+
this._endElement();
122+
return null;
123+
}
124+
visitBeginComponent(cmd: RenderBeginComponentCmd, context: any): any {
125+
var el = this._beginElement(cmd);
126+
var root = el;
127+
if (cmd.nativeShadow) {
128+
root = this.factory.createShadowRoot(el);
129+
this.nativeShadowRoots.push(root);
130+
}
131+
this.parentStack.push(new Component(el, root, cmd, this.factory));
132+
return null;
133+
}
134+
visitEndComponent(context: any): any {
135+
var c = <Component<N>>this.parent;
136+
var template = this.factory.resolveComponentTemplate(c.cmd.templateId);
137+
this._visitChildTemplate(template, c, c.shadowRoot);
138+
this._endElement();
139+
return null;
140+
}
141+
visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: any): any {
142+
var el = this.factory.createTemplateAnchor(cmd.attrNameAndValues);
143+
this._addChild(el, cmd.ngContentIndex);
144+
this.boundElements.push(el);
145+
if (cmd.isMerged) {
146+
this._visitChildTemplate(cmd.children, this.parentComponent, null);
147+
}
148+
return null;
149+
}
150+
151+
private _beginElement(cmd: RenderBeginElementCmd): N {
152+
var el: N;
153+
if (isPresent(this.inplaceElement)) {
154+
el = this.inplaceElement;
155+
this.inplaceElement = null;
156+
this.factory.mergeElement(el, cmd.attrNameAndValues);
157+
this.fragmentRootNodes.push(el);
158+
} else {
159+
el = this.factory.createElement(cmd.name, cmd.attrNameAndValues);
160+
this._addChild(el, cmd.ngContentIndex);
161+
}
162+
if (cmd.isBound) {
163+
this.boundElements.push(el);
164+
for (var i = 0; i < cmd.eventTargetAndNames.length; i += 2) {
165+
var target = cmd.eventTargetAndNames[i];
166+
var eventName = cmd.eventTargetAndNames[i + 1];
167+
this.eventData.push([this.boundElements.length - 1, target, eventName]);
168+
}
169+
}
170+
return el;
171+
}
172+
173+
private _endElement() { this.parentStack.pop(); }
174+
175+
private _visitChildTemplate(cmds: RenderTemplateCmd[], parent: Component<N>, rootNodesParent: N) {
176+
visitAll(new RenderViewBuilder(parent, rootNodesParent, null, this.allBuilders, this.factory),
177+
cmds);
178+
}
179+
180+
private _addChild(node: N, ngContentIndex: number) {
181+
var parent = this.parent;
182+
if (isPresent(parent)) {
183+
if (parent instanceof Component) {
184+
parent.addContentNode(ngContentIndex, node);
185+
} else {
186+
this.factory.appendChild(<N>parent, node);
187+
}
188+
} else {
189+
this.fragmentRootNodes.push(node);
190+
}
191+
}
192+
}
193+
194+
class Component<N> {
195+
private contentNodesByNgContentIndex: N[][] = [];
196+
private projectingNgContentIndex: number = 0;
197+
198+
constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd,
199+
public factory: NodeFactory<N>) {}
200+
addContentNode(ngContentIndex: number, node: N) {
201+
if (isBlank(ngContentIndex)) {
202+
if (this.cmd.nativeShadow) {
203+
this.factory.appendChild(this.hostElement, node);
204+
}
205+
} else {
206+
while (this.contentNodesByNgContentIndex.length <= ngContentIndex) {
207+
this.contentNodesByNgContentIndex.push([]);
208+
}
209+
this.contentNodesByNgContentIndex[ngContentIndex].push(node);
210+
}
211+
}
212+
project(): N[] {
213+
var ngContentIndex = this.projectingNgContentIndex++;
214+
return ngContentIndex < this.contentNodesByNgContentIndex.length ?
215+
this.contentNodesByNgContentIndex[ngContentIndex] :
216+
[];
217+
}
218+
}
219+
220+
function addAll(source: any[], target: any[]) {
221+
for (var i = 0; i < source.length; i++) {
222+
target.push(source[i]);
223+
}
224+
}
225+
226+
function visitAll(visitor: RenderCommandVisitor, fragmentCmds: RenderTemplateCmd[]) {
227+
for (var i = 0; i < fragmentCmds.length; i++) {
228+
fragmentCmds[i].visit(visitor, null);
229+
}
230+
}

modules/angular2/src/test_lib/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export function stringifyElement(el): string {
119119
result += '>';
120120

121121
// Children
122-
var children = DOM.childNodes(DOM.templateAwareRoot(el));
122+
var childrenRoot = DOM.templateAwareRoot(el);
123+
var children = isPresent(childrenRoot) ? DOM.childNodes(childrenRoot) : [];
123124
for (let j = 0; j < children.length; j++) {
124125
result += stringifyElement(children[j]);
125126
}

modules/angular2/test/core/dom/dom_adapter_spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ export function main() {
5050

5151
});
5252

53+
it('should be able to create text nodes and use them with the other APIs', () => {
54+
var t = DOM.createTextNode('hello');
55+
expect(DOM.isTextNode(t)).toBe(true);
56+
var d = DOM.createElement('div');
57+
DOM.appendChild(d, t);
58+
expect(DOM.getInnerHTML(d)).toEqual('hello');
59+
});
60+
61+
it('should set className via the class attribute', () => {
62+
var d = DOM.createElement('div');
63+
DOM.setAttribute(d, 'class', 'class1');
64+
expect(d.className).toEqual('class1');
65+
});
66+
67+
it('should allow to remove nodes without parents', () => {
68+
var d = DOM.createElement('div');
69+
expect(() => DOM.remove(d)).not.toThrow();
70+
});
71+
5372
if (DOM.supportsDOMEvents()) {
5473
describe('getBaseHref', () => {
5574
beforeEach(() => DOM.resetBaseElement());

0 commit comments

Comments
 (0)
X Tutup