Skip to content

Commit 0dbdd5c

Browse files
committed
refactor(render): don’t store DOM nodes but store strings for big ProtoViews.
Also inserts comment nodes before/after projected nodes so that text nodes don’t get merged when we serialize/deserialize them. Closes angular#3356 First part of angular#3364
1 parent c084039 commit 0dbdd5c

14 files changed

Lines changed: 250 additions & 40 deletions

File tree

‎modules/angular2/src/dom/browser_adapter.dart‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
238238
void setChecked(InputElement el, bool isChecked) {
239239
el.checked = isChecked;
240240
}
241+
Comment createComment(String text) {
242+
return new Comment(text);
243+
}
241244
TemplateElement createTemplate(String html) {
242245
var t = new TemplateElement();
243246
// We do not sanitize because templates are part of the application code
@@ -341,6 +344,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
341344
Node importIntoDoc(Node node) {
342345
return document.importNode(node, true);
343346
}
347+
Node adoptNode(Node node) {
348+
return document.adoptNode(node);
349+
}
344350
bool isPageRule(CssRule rule) => rule is CssPageRule;
345351
bool isStyleRule(CssRule rule) => rule is CssStyleRule;
346352
bool isMediaRule(CssRule rule) => rule is CssMediaRule;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
153153
setValue(el, value: string) { el.value = value; }
154154
getChecked(el): boolean { return el.checked; }
155155
setChecked(el, value: boolean) { el.checked = value; }
156+
createComment(text: string): Comment { return document.createComment(text); }
156157
createTemplate(html): HTMLElement {
157158
var t = document.createElement('template');
158159
t.innerHTML = html;
@@ -238,6 +239,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
238239
}
239240
return document.importNode(toImport, true);
240241
}
242+
adoptNode(node: Node): any { return document.adoptNode(node); }
241243
isPageRule(rule): boolean { return rule.type === CSSRule.PAGE_RULE; }
242244
isStyleRule(rule): boolean { return rule.type === CSSRule.STYLE_RULE; }
243245
isMediaRule(rule): boolean { return rule.type === CSSRule.MEDIA_RULE; }

‎modules/angular2/src/dom/dom_adapter.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class DomAdapter {
6969
setValue(el, value: string) { throw _abstract(); }
7070
getChecked(el): boolean { throw _abstract(); }
7171
setChecked(el, value: boolean) { throw _abstract(); }
72+
createComment(text: string): any { throw _abstract(); }
7273
createTemplate(html): HTMLElement { throw _abstract(); }
7374
createElement(tagName, doc = null): HTMLElement { throw _abstract(); }
7475
createTextNode(text: string, doc = null): Text { throw _abstract(); }
@@ -110,6 +111,7 @@ export class DomAdapter {
110111
hasShadowRoot(node): boolean { throw _abstract(); }
111112
isShadowRoot(node): boolean { throw _abstract(); }
112113
importIntoDoc /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/ { throw _abstract(); }
114+
adoptNode /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/ { throw _abstract(); }
113115
isPageRule(rule): boolean { throw _abstract(); }
114116
isStyleRule(rule): boolean { throw _abstract(); }
115117
isMediaRule(rule): boolean { throw _abstract(); }

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ class Html5LibDomAdapter implements DomAdapter {
184184
setChecked(el, bool value) {
185185
throw 'not implemented';
186186
}
187+
createComment(String text) => new Comment(text);
187188
createTemplate(String html) => createElement('template')..innerHtml = html;
188189
createElement(tagName, [doc]) {
189190
return new Element.tag(tagName);
@@ -292,6 +293,9 @@ class Html5LibDomAdapter implements DomAdapter {
292293
importIntoDoc(node) {
293294
throw 'not implemented';
294295
}
296+
adoptNode(node) {
297+
throw 'not implemented';
298+
}
295299
bool isPageRule(rule) {
296300
throw 'not implemented';
297301
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export class Parse5DomAdapter extends DomAdapter {
7777
return res;
7878
}
7979
elementMatches(node, selector: string, matcher = null): boolean {
80+
if (!selector || selector === '*') {
81+
return true;
82+
}
8083
var result = false;
8184
if (selector && selector.charAt(0) == "#") {
8285
result = this.getAttribute(node, 'id') == selector.substring(1);
@@ -252,6 +255,7 @@ export class Parse5DomAdapter extends DomAdapter {
252255
setValue(el, value: string) { el.value = value; }
253256
getChecked(el): boolean { return el.checked; }
254257
setChecked(el, value: boolean) { el.checked = value; }
258+
createComment(text: string): Comment { return treeAdapter.createCommentNode(text); }
255259
createTemplate(html): HTMLElement {
256260
var template = treeAdapter.createElement("template", 'http://www.w3.org/1999/xhtml', []);
257261
var content = parser.parseFragment(html);
@@ -447,6 +451,7 @@ export class Parse5DomAdapter extends DomAdapter {
447451
hasShadowRoot(node): boolean { return isPresent(node.shadowRoot); }
448452
isShadowRoot(node): boolean { return this.getShadowRoot(node) == node; }
449453
importIntoDoc(node): any { return this.clone(node); }
454+
adoptNode(node): any { return node; }
450455
isPageRule(rule): boolean {
451456
return rule.type === 6; // CSSRule.PAGE_RULE
452457
}

‎modules/angular2/src/render/dom/util.ts‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export const EVENT_TARGET_SEPARATOR = ':';
1212
export const NG_CONTENT_ELEMENT_NAME = 'ng-content';
1313
export const NG_SHADOW_ROOT_ELEMENT_NAME = 'shadow-root';
1414

15+
const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE = 20;
16+
1517
var CAMEL_CASE_REGEXP = /([A-Z])/g;
1618
var DASH_CASE_REGEXP = /-([a-z])/g;
1719

@@ -57,8 +59,7 @@ export class ClonedProtoView {
5759

5860
export function cloneAndQueryProtoView(pv: DomProtoView, importIntoDocument: boolean):
5961
ClonedProtoView {
60-
var templateContent = importIntoDocument ? DOM.importIntoDoc(DOM.content(pv.rootElement)) :
61-
DOM.clone(DOM.content(pv.rootElement));
62+
var templateContent = pv.cloneableTemplate.clone(importIntoDocument);
6263

6364
var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment);
6465
var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements,
@@ -140,3 +141,45 @@ export function prependAll(parentNode: Node, nodes: Node[]) {
140141
lastInsertedNode = node;
141142
});
142143
}
144+
145+
export interface CloneableTemplate { clone(importIntoDoc: boolean): Node; }
146+
147+
export class SerializedCloneableTemplate implements CloneableTemplate {
148+
templateString: string;
149+
constructor(templateRoot: Element) { this.templateString = DOM.getInnerHTML(templateRoot); }
150+
clone(importIntoDoc: boolean): Node {
151+
var result = DOM.content(DOM.createTemplate(this.templateString));
152+
if (importIntoDoc) {
153+
result = DOM.adoptNode(result);
154+
}
155+
return result;
156+
}
157+
}
158+
159+
export class ReferenceCloneableTemplate implements CloneableTemplate {
160+
constructor(public templateRoot: Element) {}
161+
clone(importIntoDoc: boolean): Node {
162+
if (importIntoDoc) {
163+
return DOM.importIntoDoc(DOM.content(this.templateRoot));
164+
} else {
165+
return DOM.clone(DOM.content(this.templateRoot));
166+
}
167+
}
168+
}
169+
170+
export function prepareTemplateForClone(templateRoot: Element): CloneableTemplate {
171+
var root = DOM.content(templateRoot);
172+
var elementCount = DOM.querySelectorAll(root, '*').length;
173+
var firstChild = DOM.firstChild(root);
174+
var forceSerialize =
175+
isPresent(firstChild) && DOM.isCommentNode(firstChild) ? DOM.nodeValue(firstChild) : null;
176+
if (forceSerialize == 'nocache') {
177+
return new SerializedCloneableTemplate(templateRoot);
178+
} else if (forceSerialize == 'cache') {
179+
return new ReferenceCloneableTemplate(templateRoot);
180+
} else if (elementCount > MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE) {
181+
return new SerializedCloneableTemplate(templateRoot);
182+
} else {
183+
return new ReferenceCloneableTemplate(templateRoot);
184+
}
185+
}

‎modules/angular2/src/render/dom/view/proto_view.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {RenderProtoViewRef, ViewType, ViewEncapsulation} from '../../api';
55

66
import {DOM} from 'angular2/src/dom/dom_adapter';
77

8+
import {prepareTemplateForClone, CloneableTemplate} from '../util';
9+
810
export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView {
911
return (<DomProtoViewRef>protoViewRef)._protoView;
1012
}
@@ -25,12 +27,12 @@ export class DomProtoView {
2527
var isSingleElementFragment = fragmentsRootNodeCount.length === 1 &&
2628
fragmentsRootNodeCount[0] === 1 &&
2729
DOM.isElementNode(DOM.firstChild(DOM.content(rootElement)));
28-
return new DomProtoView(type, rootElement, viewEncapsulation, elementBinders, hostAttributes,
29-
rootTextNodeIndices, boundTextNodeCount, fragmentsRootNodeCount,
30-
isSingleElementFragment);
30+
return new DomProtoView(type, prepareTemplateForClone(rootElement), viewEncapsulation,
31+
elementBinders, hostAttributes, rootTextNodeIndices, boundTextNodeCount,
32+
fragmentsRootNodeCount, isSingleElementFragment);
3133
}
3234

33-
constructor(public type: ViewType, public rootElement: Element,
35+
constructor(public type: ViewType, public cloneableTemplate: CloneableTemplate,
3436
public encapsulation: ViewEncapsulation,
3537
public elementBinders: List<DomElementBinder>,
3638
public hostAttributes: Map<string, string>, public rootTextNodeIndices: number[],

‎modules/angular2/src/render/dom/view/proto_view_merger.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ function appendComponentNodesToHost(hostProtoView: ClonedProtoView, binderIdx: n
251251

252252
function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] {
253253
var remaining = [];
254+
DOM.insertBefore(contentElement, DOM.createComment('['));
254255
for (var i = 0; i < nodes.length; i++) {
255256
var node = nodes[i];
256257
var matches = false;
@@ -265,6 +266,7 @@ function projectMatchingNodes(selector: string, contentElement: Element, nodes:
265266
remaining.push(node);
266267
}
267268
}
269+
DOM.insertBefore(contentElement, DOM.createComment(']'));
268270
DOM.remove(contentElement);
269271
return remaining;
270272
}

‎modules/angular2/src/test_lib/utils.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export function stringifyElement(el): string {
7777
if (!ListWrapper.contains(_singleTagWhitelist, tagName)) {
7878
result += `</${tagName}>`;
7979
}
80+
} else if (DOM.isCommentNode(el)) {
81+
result += `<!--${DOM.nodeValue(el)}-->`;
8082
} else {
8183
result += DOM.getText(el);
8284
}

‎modules/angular2/test/core/compiler/integration_spec.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,28 @@ export function main() {
13901390
});
13911391
}
13921392

1393+
describe('different proto view storages', () => {
1394+
function runWithMode(mode: string) {
1395+
return inject(
1396+
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
1397+
tcb.overrideView(MyComp,
1398+
new viewAnn.View({template: `<!--${mode}--><div>{{ctxProp}}</div>`}))
1399+
.createAsync(MyComp)
1400+
.then((rootTC) => {
1401+
rootTC.componentInstance.ctxProp = 'Hello World!';
1402+
1403+
rootTC.detectChanges();
1404+
expect(rootTC.nativeElement).toHaveText('Hello World!');
1405+
async.done();
1406+
});
1407+
});
1408+
}
1409+
1410+
it('should work with storing DOM nodes', runWithMode('cache'));
1411+
1412+
it('should work with serializing the DOM nodes', runWithMode('nocache'));
1413+
});
1414+
13931415
// Disabled until a solution is found, refs:
13941416
// - https://github.com/angular/angular/issues/776
13951417
// - https://github.com/angular/angular/commit/81f3f32

0 commit comments

Comments
 (0)