Skip to content

Commit

Permalink
fix(render): allow to configure when templates are serialized to strings
Browse files Browse the repository at this point in the history
Introduces the injectable `TemplateCloner` that can be configured via the new token `MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN`.

Also replaces `document.adoptNode` with `document.importNode` as otherwise 
custom elements are not triggered in chrome 43.

Closes angular#3418
  • Loading branch information
tbosch committed Jul 31, 2015
1 parent f8fa47e commit 5c7b79f
Show file tree
Hide file tree
Showing 24 changed files with 310 additions and 222 deletions.
3 changes: 2 additions & 1 deletion modules/angular2/render.ts
Expand Up @@ -17,5 +17,6 @@ export {
ViewDefinition,
DOCUMENT_TOKEN,
APP_ID_TOKEN,
DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES
DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES,
MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN
} from './src/render/render';
6 changes: 5 additions & 1 deletion modules/angular2/src/core/application_common.ts
Expand Up @@ -59,7 +59,9 @@ import {
DOCUMENT_TOKEN,
DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES,
DefaultDomCompiler,
APP_ID_RANDOM_BINDING
APP_ID_RANDOM_BINDING,
MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN,
TemplateCloner
} from 'angular2/src/render/render';
import {ElementSchemaRegistry} from 'angular2/src/render/dom/schema/element_schema_registry';
import {DomElementSchemaRegistry} from 'angular2/src/render/dom/schema/dom_element_schema_registry';
Expand Down Expand Up @@ -114,6 +116,8 @@ function _injectorBindings(appComponentType): List<Type | Binding | List<any>> {
DomRenderer,
bind(Renderer).toAlias(DomRenderer),
APP_ID_RANDOM_BINDING,
TemplateCloner,
bind(MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN).toValue(20),
DefaultDomCompiler,
bind(ElementSchemaRegistry).toValue(new DomElementSchemaRegistry()),
bind(RenderCompiler).toAlias(DefaultDomCompiler),
Expand Down
2 changes: 1 addition & 1 deletion modules/angular2/src/dom/parse5_adapter.ts
Expand Up @@ -77,7 +77,7 @@ export class Parse5DomAdapter extends DomAdapter {
return res;
}
elementMatches(node, selector: string, matcher = null): boolean {
if (!selector || selector === '*') {
if (this.isElementNode(node) && selector === '*') {
return true;
}
var result = false;
Expand Down
18 changes: 11 additions & 7 deletions modules/angular2/src/render/dom/compiler/compiler.ts
Expand Up @@ -24,6 +24,7 @@ import {DOCUMENT_TOKEN, APP_ID_TOKEN} from '../dom_tokens';
import {Inject} from 'angular2/di';
import {SharedStylesHost} from '../view/shared_styles_host';
import {prependAll} from '../util';
import {TemplateCloner} from '../template_cloner';

/**
* The compiler loads and translates the html templates of components into
Expand All @@ -32,8 +33,8 @@ import {prependAll} from '../util';
*/
export class DomCompiler extends RenderCompiler {
constructor(private _schemaRegistry: ElementSchemaRegistry,
private _stepFactory: CompileStepFactory, private _viewLoader: ViewLoader,
private _sharedStylesHost: SharedStylesHost) {
private _templateCloner: TemplateCloner, private _stepFactory: CompileStepFactory,
private _viewLoader: ViewLoader, private _sharedStylesHost: SharedStylesHost) {
super();
}

Expand Down Expand Up @@ -65,7 +66,8 @@ export class DomCompiler extends RenderCompiler {

mergeProtoViewsRecursively(
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
return PromiseWrapper.resolve(pvm.mergeProtoViewsRecursively(protoViewRefs));
return PromiseWrapper.resolve(
pvm.mergeProtoViewsRecursively(this._templateCloner, protoViewRefs));
}

_compileView(viewDef: ViewDefinition, templateAndStyles: TemplateAndStyles,
Expand All @@ -87,7 +89,7 @@ export class DomCompiler extends RenderCompiler {
}

return PromiseWrapper.resolve(
compileElements[0].inheritedProtoView.build(this._schemaRegistry));
compileElements[0].inheritedProtoView.build(this._schemaRegistry, this._templateCloner));
}

_normalizeViewEncapsulationIfThereAreNoStyles(viewDef: ViewDefinition): ViewDefinition {
Expand All @@ -108,8 +110,10 @@ export class DomCompiler extends RenderCompiler {

@Injectable()
export class DefaultDomCompiler extends DomCompiler {
constructor(schemaRegistry: ElementSchemaRegistry, parser: Parser, viewLoader: ViewLoader,
sharedStylesHost: SharedStylesHost, @Inject(APP_ID_TOKEN) appId: any) {
super(schemaRegistry, new DefaultStepFactory(parser, appId), viewLoader, sharedStylesHost);
constructor(schemaRegistry: ElementSchemaRegistry, templateCloner: TemplateCloner, parser: Parser,
viewLoader: ViewLoader, sharedStylesHost: SharedStylesHost,
@Inject(APP_ID_TOKEN) appId: any) {
super(schemaRegistry, templateCloner, new DefaultStepFactory(parser, appId), viewLoader,
sharedStylesHost);
}
}
9 changes: 6 additions & 3 deletions modules/angular2/src/render/dom/dom_renderer.ts
Expand Up @@ -32,6 +32,8 @@ import {
RenderViewWithFragments
} from '../api';

import {TemplateCloner} from './template_cloner';

import {DOCUMENT_TOKEN, DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES} from './dom_tokens';

const REFLECT_PREFIX: string = 'ng-reflect-';
Expand All @@ -41,8 +43,9 @@ export class DomRenderer extends Renderer {
_document;
_reflectPropertiesAsAttributes: boolean;

constructor(public _eventManager: EventManager, private _domSharedStylesHost: DomSharedStylesHost,
@Inject(DOCUMENT_TOKEN) document,
constructor(private _eventManager: EventManager,
private _domSharedStylesHost: DomSharedStylesHost,
private _templateCloner: TemplateCloner, @Inject(DOCUMENT_TOKEN) document,
@Inject(DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES) reflectPropertiesAsAttributes:
boolean) {
super();
Expand Down Expand Up @@ -206,7 +209,7 @@ export class DomRenderer extends Renderer {
}

_createView(protoView: DomProtoView, inplaceElement: HTMLElement): RenderViewWithFragments {
var clonedProtoView = cloneAndQueryProtoView(protoView, true);
var clonedProtoView = cloneAndQueryProtoView(this._templateCloner, protoView, true);

var boundElements = clonedProtoView.boundElements;

Expand Down
6 changes: 6 additions & 0 deletions modules/angular2/src/render/dom/dom_tokens.ts
Expand Up @@ -18,6 +18,12 @@ export const APP_ID_TOKEN: OpaqueToken = CONST_EXPR(new OpaqueToken('AppId'));
export var APP_ID_RANDOM_BINDING: Binding =
bind(APP_ID_TOKEN).toFactory(() => `${randomChar()}${randomChar()}${randomChar()}`, []);

/**
* Defines when a compiled template should be stored as a string
* rather than keeping its Nodes to preserve memory.
*/
export const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN: OpaqueToken =
CONST_EXPR(new OpaqueToken('MaxInMemoryElementsPerTemplate'));

function randomChar(): string {
return StringWrapper.fromCharCode(97 + Math.floor(Math.random() * 25));
Expand Down
47 changes: 47 additions & 0 deletions modules/angular2/src/render/dom/template_cloner.ts
@@ -0,0 +1,47 @@
import {isString} from 'angular2/src/facade/lang';
import {Injectable, Inject} from 'angular2/di';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN} from './dom_tokens';

@Injectable()
export class TemplateCloner {
maxInMemoryElementsPerTemplate: number;

constructor(@Inject(MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN) maxInMemoryElementsPerTemplate) {
this.maxInMemoryElementsPerTemplate = maxInMemoryElementsPerTemplate;
}

prepareForClone(templateRoot: Element): Element | string {
var elementCount = DOM.querySelectorAll(DOM.content(templateRoot), '*').length;
if (this.maxInMemoryElementsPerTemplate >= 0 &&
elementCount >= this.maxInMemoryElementsPerTemplate) {
return DOM.getInnerHTML(templateRoot);
} else {
return templateRoot;
}
}

cloneContent(preparedTemplateRoot: Element | string, importNode: boolean): Node {
var templateContent;
if (isString(preparedTemplateRoot)) {
templateContent = DOM.content(DOM.createTemplate(preparedTemplateRoot));
if (importNode) {
// Attention: We can't use document.adoptNode here
// as this does NOT wake up custom elements in Chrome 43
// TODO: Use div.innerHTML instead of template.innerHTML when we
// have code to support the various special cases and
// don't use importNode additionally (e.g. for <tr>, svg elements, ...)
// see https://github.com/angular/angular/issues/3364
templateContent = DOM.importIntoDoc(templateContent);
}
} else {
templateContent = DOM.content(<Element>preparedTemplateRoot);
if (importNode) {
templateContent = DOM.importIntoDoc(templateContent);
} else {
templateContent = DOM.clone(templateContent);
}
}
return templateContent;
}
}
53 changes: 8 additions & 45 deletions modules/angular2/src/render/dom/util.ts
Expand Up @@ -3,6 +3,7 @@ import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper} from 'angular2/src/facade/collection';
import {DomProtoView} from './view/proto_view';
import {DomElementBinder} from './view/element_binder';
import {TemplateCloner} from './template_cloner';

export const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
export const NG_BINDING_CLASS = 'ng-binding';
Expand Down Expand Up @@ -57,9 +58,9 @@ export class ClonedProtoView {
public boundElements: Element[], public boundTextNodes: Node[]) {}
}

export function cloneAndQueryProtoView(pv: DomProtoView, importIntoDocument: boolean):
ClonedProtoView {
var templateContent = pv.cloneableTemplate.clone(importIntoDocument);
export function cloneAndQueryProtoView(templateCloner: TemplateCloner, pv: DomProtoView,
importIntoDocument: boolean): ClonedProtoView {
var templateContent = templateCloner.cloneContent(pv.cloneableTemplate, importIntoDocument);

var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment);
var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements,
Expand All @@ -77,6 +78,10 @@ function queryFragments(templateContent: Node, fragmentsRootNodeCount: number[])
for (var fragmentIndex = 0; fragmentIndex < fragments.length; fragmentIndex++) {
var fragment = ListWrapper.createFixedSize(fragmentsRootNodeCount[fragmentIndex]);
fragments[fragmentIndex] = fragment;
// Note: the 2nd, 3rd, ... fragments are separated by each other via a '|'
if (fragmentIndex >= 1) {
childNode = DOM.nextSibling(childNode);
}
for (var i = 0; i < fragment.length; i++) {
fragment[i] = childNode;
childNode = DOM.nextSibling(childNode);
Expand Down Expand Up @@ -141,45 +146,3 @@ export function prependAll(parentNode: Node, nodes: Node[]) {
lastInsertedNode = node;
});
}

export interface CloneableTemplate { clone(importIntoDoc: boolean): Node; }

export class SerializedCloneableTemplate implements CloneableTemplate {
templateString: string;
constructor(templateRoot: Element) { this.templateString = DOM.getInnerHTML(templateRoot); }
clone(importIntoDoc: boolean): Node {
var result = DOM.content(DOM.createTemplate(this.templateString));
if (importIntoDoc) {
result = DOM.adoptNode(result);
}
return result;
}
}

export class ReferenceCloneableTemplate implements CloneableTemplate {
constructor(public templateRoot: Element) {}
clone(importIntoDoc: boolean): Node {
if (importIntoDoc) {
return DOM.importIntoDoc(DOM.content(this.templateRoot));
} else {
return DOM.clone(DOM.content(this.templateRoot));
}
}
}

export function prepareTemplateForClone(templateRoot: Element): CloneableTemplate {
var root = DOM.content(templateRoot);
var elementCount = DOM.querySelectorAll(root, '*').length;
var firstChild = DOM.firstChild(root);
var forceSerialize =
isPresent(firstChild) && DOM.isCommentNode(firstChild) ? DOM.nodeValue(firstChild) : null;
if (forceSerialize == 'nocache') {
return new SerializedCloneableTemplate(templateRoot);
} else if (forceSerialize == 'cache') {
return new ReferenceCloneableTemplate(templateRoot);
} else if (elementCount > MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE) {
return new SerializedCloneableTemplate(templateRoot);
} else {
return new ReferenceCloneableTemplate(templateRoot);
}
}
14 changes: 7 additions & 7 deletions modules/angular2/src/render/dom/view/proto_view.ts
Expand Up @@ -5,7 +5,7 @@ import {RenderProtoViewRef, ViewType, ViewEncapsulation} from '../../api';

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

import {prepareTemplateForClone, CloneableTemplate} from '../util';
import {TemplateCloner} from '../template_cloner';

export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView {
return (<DomProtoViewRef>protoViewRef)._protoView;
Expand All @@ -16,9 +16,9 @@ export class DomProtoViewRef extends RenderProtoViewRef {
}

export class DomProtoView {
static create(type: ViewType, rootElement: Element, viewEncapsulation: ViewEncapsulation,
fragmentsRootNodeCount: number[], rootTextNodeIndices: number[],
elementBinders: List<DomElementBinder>,
static create(templateCloner: TemplateCloner, type: ViewType, rootElement: Element,
viewEncapsulation: ViewEncapsulation, fragmentsRootNodeCount: number[],
rootTextNodeIndices: number[], elementBinders: List<DomElementBinder>,
hostAttributes: Map<string, string>): DomProtoView {
var boundTextNodeCount = rootTextNodeIndices.length;
for (var i = 0; i < elementBinders.length; i++) {
Expand All @@ -27,12 +27,12 @@ export class DomProtoView {
var isSingleElementFragment = fragmentsRootNodeCount.length === 1 &&
fragmentsRootNodeCount[0] === 1 &&
DOM.isElementNode(DOM.firstChild(DOM.content(rootElement)));
return new DomProtoView(type, prepareTemplateForClone(rootElement), viewEncapsulation,
return new DomProtoView(type, templateCloner.prepareForClone(rootElement), viewEncapsulation,
elementBinders, hostAttributes, rootTextNodeIndices, boundTextNodeCount,
fragmentsRootNodeCount, isSingleElementFragment);
}

constructor(public type: ViewType, public cloneableTemplate: CloneableTemplate,
// Note: fragments are separated by a comment node that is not counted in fragmentsRootNodeCount!
constructor(public type: ViewType, public cloneableTemplate: Element | string,
public encapsulation: ViewEncapsulation,
public elementBinders: List<DomElementBinder>,
public hostAttributes: Map<string, string>, public rootTextNodeIndices: number[],
Expand Down
14 changes: 8 additions & 6 deletions modules/angular2/src/render/dom/view/proto_view_builder.ts
Expand Up @@ -21,6 +21,7 @@ import {
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {DomElementBinder, Event, HostAction} from './element_binder';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {TemplateCloner} from '../template_cloner';

import * as api from '../../api';

Expand Down Expand Up @@ -69,7 +70,7 @@ export class ProtoViewBuilder {

setHostAttribute(name: string, value: string) { this.hostAttributes.set(name, value); }

build(schemaRegistry: ElementSchemaRegistry): api.ProtoViewDto {
build(schemaRegistry: ElementSchemaRegistry, templateCloner: TemplateCloner): api.ProtoViewDto {
var domElementBinders = [];

var apiElementBinders = [];
Expand All @@ -96,8 +97,9 @@ export class ProtoViewBuilder {
dbb.hostPropertyBindings, new Set())
});
});
var nestedProtoView =
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build(schemaRegistry) : null;
var nestedProtoView = isPresent(ebb.nestedProtoView) ?
ebb.nestedProtoView.build(schemaRegistry, templateCloner) :
null;
if (isPresent(nestedProtoView)) {
transitiveNgContentCount += nestedProtoView.transitiveNgContentCount;
}
Expand Down Expand Up @@ -131,9 +133,9 @@ export class ProtoViewBuilder {
});
var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length;
return new api.ProtoViewDto({
render: new DomProtoViewRef(
DomProtoView.create(this.type, this.rootElement, this.viewEncapsulation, [rootNodeCount],
rootTextNodeIndices, domElementBinders, this.hostAttributes)),
render: new DomProtoViewRef(DomProtoView.create(
templateCloner, this.type, this.rootElement, this.viewEncapsulation, [rootNodeCount],
rootTextNodeIndices, domElementBinders, this.hostAttributes)),
type: this.type,
elementBinders: apiElementBinders,
variableBindings: this.variableBindings,
Expand Down

0 comments on commit 5c7b79f

Please sign in to comment.