fix(render): allow to configure when templates are serialized to strings

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 #3418
Closes #3433
This commit is contained in:
Tobias Bosch
2015-07-31 10:58:24 -07:00
parent 014b6cb397
commit dd06a871b7
24 changed files with 310 additions and 222 deletions

View File

@ -38,6 +38,8 @@ import {
ViewEncapsulation
} from 'angular2/angular2';
import {MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN} from 'angular2/src/render/render';
export function main() {
describe('projection', () => {
it('should support simple components',
@ -416,6 +418,44 @@ export function main() {
}));
}
describe('different proto view storages', () => {
function runTests() {
it('should support nested conditionals that contain ng-contents',
inject(
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb.overrideView(MainComp, new viewAnn.View({
template: `<conditional-text>a</conditional-text>`,
directives: [ConditionalTextComponent]
}))
.createAsync(MainComp)
.then((main) => {
expect(main.nativeElement).toHaveText('MAIN()');
var viewportElement = main.componentViewChildren[0].componentViewChildren[0];
viewportElement.inject(ManualViewportDirective).show();
expect(main.nativeElement).toHaveText('MAIN(FIRST())');
viewportElement = main.componentViewChildren[0].componentViewChildren[1];
viewportElement.inject(ManualViewportDirective).show();
expect(main.nativeElement).toHaveText('MAIN(FIRST(SECOND(a)))');
async.done();
});
}));
}
describe('serialize templates', () => {
beforeEachBindings(() => [bind(MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN).toValue(0)]);
runTests();
});
describe("don't serialize templates", () => {
beforeEachBindings(() => [bind(MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE_TOKEN).toValue(-1)]);
runTests();
});
});
});
}
@ -508,6 +548,15 @@ class InnerInnerComponent {
class ConditionalContentComponent {
}
@Component({selector: 'conditional-text'})
@View({
template:
'MAIN(<template manual>FIRST(<template manual>SECOND(<ng-content></ng-content>)</template>)</template>)',
directives: [ManualViewportDirective]
})
class ConditionalTextComponent {
}
@Component({selector: 'tab'})
@View({
template: '<div><div *manual>TAB(<ng-content></ng-content>)</div></div>',

View File

@ -31,11 +31,10 @@ import {ViewLoader, TemplateAndStyles} from 'angular2/src/render/dom/compiler/vi
import {resolveInternalDomProtoView} from 'angular2/src/render/dom/view/proto_view';
import {SharedStylesHost} from 'angular2/src/render/dom/view/shared_styles_host';
import {TemplateCloner} from 'angular2/src/render/dom/template_cloner';
import {MockStep} from './pipeline_spec';
import {ReferenceCloneableTemplate} from 'angular2/src/render/dom/util';
export function runCompilerCommonTests() {
describe('DomCompiler', function() {
var mockStepFactory: MockStepFactory;
@ -51,8 +50,8 @@ export function runCompilerCommonTests() {
var tplLoader = new FakeViewLoader(urlData);
mockStepFactory =
new MockStepFactory([new MockStep(processElementClosure, processStyleClosure)]);
return new DomCompiler(new ElementSchemaRegistry(), mockStepFactory, tplLoader,
sharedStylesHost);
return new DomCompiler(new ElementSchemaRegistry(), new TemplateCloner(-1), mockStepFactory,
tplLoader, sharedStylesHost);
}
describe('compile', () => {
@ -255,9 +254,9 @@ export function runCompilerCommonTests() {
});
}
function templateRoot(protoViewDto: ProtoViewDto) {
function templateRoot(protoViewDto: ProtoViewDto): Element {
var pv = resolveInternalDomProtoView(protoViewDto.render);
return (<ReferenceCloneableTemplate>pv.cloneableTemplate).templateRoot;
return (<Element>pv.cloneableTemplate);
}
class MockStepFactory extends CompileStepFactory {

View File

@ -0,0 +1,67 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
beforeEachBindings,
SpyObject,
} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {TemplateCloner} from 'angular2/src/render/dom/template_cloner';
export function main() {
describe('TemplateCloner', () => {
var cloner: TemplateCloner;
var bigTemplate: Element;
var smallTemplate: Element;
beforeEach(() => {
cloner = new TemplateCloner(1);
bigTemplate = DOM.createTemplate('a<div></div>');
smallTemplate = DOM.createTemplate('a');
});
describe('prepareForClone', () => {
it('should use a reference for small templates',
() => { expect(cloner.prepareForClone(smallTemplate)).toBe(smallTemplate); });
it('should use a reference if the max element count is -1', () => {
cloner = new TemplateCloner(-1);
expect(cloner.prepareForClone(bigTemplate)).toBe(bigTemplate);
});
it('should use a string for big templates', () => {
expect(cloner.prepareForClone(bigTemplate)).toEqual(DOM.getInnerHTML(bigTemplate));
});
});
describe('cloneTemplate', () => {
function shouldReturnTemplateContentNodes(template: Element, importIntoDoc: boolean) {
var clone = cloner.cloneContent(cloner.prepareForClone(template), importIntoDoc);
expect(clone).not.toBe(DOM.content(template));
expect(DOM.getText(DOM.firstChild(clone))).toEqual('a');
}
it('should return template.content nodes (small template, no import)',
() => { shouldReturnTemplateContentNodes(smallTemplate, false); });
it('should return template.content nodes (small template, import)',
() => { shouldReturnTemplateContentNodes(smallTemplate, true); });
it('should return template.content nodes (big template, no import)',
() => { shouldReturnTemplateContentNodes(bigTemplate, false); });
it('should return template.content nodes (big template, import)',
() => { shouldReturnTemplateContentNodes(bigTemplate, true); });
});
});
}

View File

@ -1,97 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
beforeEachBindings,
SpyObject,
} from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
prepareTemplateForClone,
ReferenceCloneableTemplate,
SerializedCloneableTemplate
} from 'angular2/src/render/dom/util';
export function main() {
describe('Dom util', () => {
describe('prepareTemplateForClone', () => {
it('should use a reference for small templates', () => {
var t = DOM.createTemplate('');
var ct = prepareTemplateForClone(t);
expect((<ReferenceCloneableTemplate>ct).templateRoot).toBe(t);
});
it('should use a reference for big templates with a force comment', () => {
var templateString = '<!--cache-->';
for (var i = 0; i < 100; i++) {
templateString += '<div></div>';
}
var t = DOM.createTemplate(templateString);
var ct = prepareTemplateForClone(t);
expect((<ReferenceCloneableTemplate>ct).templateRoot).toBe(t);
});
it('should serialize for big templates', () => {
var templateString = '';
for (var i = 0; i < 100; i++) {
templateString += '<div></div>';
}
var t = DOM.createTemplate(templateString);
var ct = prepareTemplateForClone(t);
expect((<SerializedCloneableTemplate>ct).templateString).toEqual(templateString);
});
it('should serialize for templates with the force comment', () => {
var templateString = '<!--nocache-->';
var t = DOM.createTemplate(templateString);
var ct = prepareTemplateForClone(t);
expect((<SerializedCloneableTemplate>ct).templateString).toEqual(templateString);
});
});
describe('ReferenceCloneableTemplate', () => {
it('should return template.content nodes (no import)', () => {
var t = DOM.createTemplate('a');
var ct = new ReferenceCloneableTemplate(t);
var clone = ct.clone(false);
expect(clone).not.toBe(DOM.content(t));
expect(DOM.getText(DOM.firstChild(clone))).toEqual('a');
});
it('should return template.content nodes (import into doc)', () => {
var t = DOM.createTemplate('a');
var ct = new ReferenceCloneableTemplate(t);
var clone = ct.clone(true);
expect(clone).not.toBe(DOM.content(t));
expect(DOM.getText(DOM.firstChild(clone))).toEqual('a');
});
});
describe('SerializedCloneableTemplate', () => {
it('should return template.content nodes (no import)', () => {
var t = DOM.createTemplate('a');
var ct = new SerializedCloneableTemplate(t);
var clone = ct.clone(false);
expect(clone).not.toBe(DOM.content(t));
expect(DOM.getText(DOM.firstChild(clone))).toEqual('a');
});
it('should return template.content nodes (import into doc)', () => {
var t = DOM.createTemplate('a');
var ct = new SerializedCloneableTemplate(t);
var clone = ct.clone(true);
expect(clone).not.toBe(DOM.content(t));
expect(DOM.getText(DOM.firstChild(clone))).toEqual('a');
});
});
});
}

View File

@ -12,6 +12,7 @@ import {
} from 'angular2/test_lib';
import {DomElementSchemaRegistry} from 'angular2/src/render/dom/schema/dom_element_schema_registry';
import {TemplateCloner} from 'angular2/src/render/dom/template_cloner';
import {ProtoViewBuilder} from 'angular2/src/render/dom/view/proto_view_builder';
import {ASTWithSource, AST} from 'angular2/src/change_detection/change_detection';
import {PropertyBindingType, ViewType, ViewEncapsulation} from 'angular2/src/render/api';
@ -22,7 +23,9 @@ export function main() {
describe('ProtoViewBuilder', () => {
var builder;
var templateCloner;
beforeEach(() => {
templateCloner = new TemplateCloner(-1);
builder =
new ProtoViewBuilder(DOM.createTemplate(''), ViewType.EMBEDDED, ViewEncapsulation.NONE);
});
@ -32,7 +35,7 @@ export function main() {
it('should throw for unknown properties', () => {
builder.bindElement(el('<div/>')).bindProperty('unknownProperty', emptyExpr());
expect(() => builder.build(new DomElementSchemaRegistry()))
expect(() => builder.build(new DomElementSchemaRegistry(), templateCloner))
.toThrowError(
`Can't bind to 'unknownProperty' since it isn't a known property of the '<div>' element and there are no matching directives with a corresponding property`);
});
@ -41,7 +44,7 @@ export function main() {
var binder = builder.bindElement(el('<div/>'));
binder.bindDirective(0).bindProperty('someDirProperty', emptyExpr(), 'directiveProperty');
binder.bindProperty('directiveProperty', emptyExpr());
expect(() => builder.build(new DomElementSchemaRegistry())).not.toThrow();
expect(() => builder.build(new DomElementSchemaRegistry(), templateCloner)).not.toThrow();
});
it('should throw for unknown host properties even if another directive uses it', () => {
@ -56,14 +59,14 @@ export function main() {
it('should allow unknown properties on custom elements', () => {
var binder = builder.bindElement(el('<some-custom/>'));
binder.bindProperty('unknownProperty', emptyExpr());
expect(() => builder.build(new DomElementSchemaRegistry())).not.toThrow();
expect(() => builder.build(new DomElementSchemaRegistry(), templateCloner)).not.toThrow();
});
it('should throw for unknown properties on custom elements if there is an ng component', () => {
var binder = builder.bindElement(el('<some-custom/>'));
binder.bindProperty('unknownProperty', emptyExpr());
binder.setComponentId('someComponent');
expect(() => builder.build(new DomElementSchemaRegistry()))
expect(() => builder.build(new DomElementSchemaRegistry(), templateCloner))
.toThrowError(
`Can't bind to 'unknownProperty' since it isn't a known property of the '<some-custom>' element and there are no matching directives with a corresponding property`);
});
@ -77,7 +80,7 @@ export function main() {
// when https://github.com/angular/angular/issues/3019 is solved.
it('should not throw for unknown properties', () => {
builder.bindElement(el('<div/>')).bindProperty('unknownProperty', emptyExpr());
expect(() => builder.build(new DomElementSchemaRegistry())).not.toThrow();
expect(() => builder.build(new DomElementSchemaRegistry(), templateCloner)).not.toThrow();
});
});
@ -86,19 +89,19 @@ export function main() {
describe('property normalization', () => {
it('should normalize "innerHtml" to "innerHTML"', () => {
builder.bindElement(el('<div/>')).bindProperty('innerHtml', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].property).toEqual('innerHTML');
});
it('should normalize "tabindex" to "tabIndex"', () => {
builder.bindElement(el('<div/>')).bindProperty('tabindex', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].property).toEqual('tabIndex');
});
it('should normalize "readonly" to "readOnly"', () => {
builder.bindElement(el('<input/>')).bindProperty('readonly', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].property).toEqual('readOnly');
});
@ -107,32 +110,32 @@ export function main() {
describe('property binding types', () => {
it('should detect property names', () => {
builder.bindElement(el('<div/>')).bindProperty('tabindex', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.PROPERTY);
});
it('should detect attribute names', () => {
builder.bindElement(el('<div/>')).bindProperty('attr.someName', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].type)
.toEqual(PropertyBindingType.ATTRIBUTE);
});
it('should detect class names', () => {
builder.bindElement(el('<div/>')).bindProperty('class.someName', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.CLASS);
});
it('should detect style names', () => {
builder.bindElement(el('<div/>')).bindProperty('style.someName', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].type).toEqual(PropertyBindingType.STYLE);
});
it('should detect style units', () => {
builder.bindElement(el('<div/>')).bindProperty('style.someName.someUnit', emptyExpr());
var pv = builder.build(new DomElementSchemaRegistry());
var pv = builder.build(new DomElementSchemaRegistry(), templateCloner);
expect(pv.elementBinders[0].propertyBindings[0].unit).toEqual('someUnit');
});
});

View File

@ -28,7 +28,8 @@ import {
} from 'angular2/src/render/api';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {cloneAndQueryProtoView, ReferenceCloneableTemplate} from 'angular2/src/render/dom/util';
import {cloneAndQueryProtoView} from 'angular2/src/render/dom/util';
import {TemplateCloner} from 'angular2/src/render/dom/template_cloner';
import {resolveInternalDomProtoView, DomProtoView} from 'angular2/src/render/dom/view/proto_view';
import {ProtoViewBuilder} from 'angular2/src/render/dom/view/proto_view_builder';
import {ElementSchemaRegistry} from 'angular2/src/render/dom/schema/element_schema_registry';
@ -255,13 +256,14 @@ export function main() {
describe('host attributes', () => {
it('should set host attributes while merging',
inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
inject([AsyncTestCompleter, DomTestbed, TemplateCloner], (async, tb: DomTestbed,
cloner: TemplateCloner) => {
tb.compiler.compileHost(rootDirective('root'))
.then((rootProtoViewDto) => {
var builder = new ProtoViewBuilder(DOM.createTemplate(''), ViewType.COMPONENT,
ViewEncapsulation.NONE);
builder.setHostAttribute('a', 'b');
var componentProtoViewDto = builder.build(new ElementSchemaRegistry());
var componentProtoViewDto = builder.build(new ElementSchemaRegistry(), cloner);
tb.merge([rootProtoViewDto, componentProtoViewDto])
.then(mergeMappings => {
var domPv = resolveInternalDomProtoView(mergeMappings.mergedProtoViewRef);
@ -278,20 +280,21 @@ export function main() {
}
function templateRoot(pv: DomProtoView) {
return (<ReferenceCloneableTemplate>pv.cloneableTemplate).templateRoot;
return <Element>pv.cloneableTemplate;
}
function runAndAssert(hostElementName: string, componentTemplates: string[],
expectedFragments: string[]) {
var useNativeEncapsulation = hostElementName.startsWith('native-');
var rootComp = rootDirective(hostElementName);
return inject([AsyncTestCompleter, DomTestbed], (async, tb: DomTestbed) => {
return inject([AsyncTestCompleter, DomTestbed, TemplateCloner], (async, tb: DomTestbed,
cloner: TemplateCloner) => {
tb.compileAndMerge(rootComp, componentTemplates.map(template => componentView(
template, useNativeEncapsulation ?
ViewEncapsulation.NATIVE :
ViewEncapsulation.NONE)))
.then((mergeMappings) => {
expect(stringify(mergeMappings)).toEqual(expectedFragments);
expect(stringify(cloner, mergeMappings)).toEqual(expectedFragments);
async.done();
});
});
@ -312,9 +315,10 @@ function componentView(template: string,
});
}
function stringify(protoViewMergeMapping: RenderProtoViewMergeMapping): string[] {
function stringify(cloner: TemplateCloner, protoViewMergeMapping: RenderProtoViewMergeMapping):
string[] {
var testView = cloneAndQueryProtoView(
resolveInternalDomProtoView(protoViewMergeMapping.mergedProtoViewRef), false);
cloner, resolveInternalDomProtoView(protoViewMergeMapping.mergedProtoViewRef), false);
for (var i = 0; i < protoViewMergeMapping.mappedElementIndices.length; i++) {
var renderElIdx = protoViewMergeMapping.mappedElementIndices[i];
if (isPresent(renderElIdx)) {

View File

@ -22,6 +22,7 @@ import {DomProtoView} from 'angular2/src/render/dom/view/proto_view';
import {DomElementBinder} from 'angular2/src/render/dom/view/element_binder';
import {DomView} from 'angular2/src/render/dom/view/view';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {TemplateCloner} from 'angular2/src/render/dom/template_cloner';
export function main() {
describe('DomView', () => {
@ -30,7 +31,8 @@ export function main() {
binders = [];
}
var rootEl = DOM.createTemplate('<div></div>');
return DomProtoView.create(null, <Element>rootEl, null, [1], [], binders, null);
return DomProtoView.create(new TemplateCloner(-1), null, <Element>rootEl, null, [1], [],
binders, null);
}
function createElementBinder() { return new DomElementBinder({textNodeIndices: []}); }

View File

@ -39,7 +39,6 @@ import {someComponent} from '../../render/dom/dom_renderer_integration_spec';
import {WebWorkerMain} from 'angular2/src/web-workers/ui/impl';
import {AnchorBasedAppRootUrl} from 'angular2/src/services/anchor_based_app_root_url';
import {MockMessageBus, MockMessageBusSink, MockMessageBusSource} from './worker_test_util';
import {ReferenceCloneableTemplate} from 'angular2/src/render/dom/util';
export function main() {
function createBroker(workerSerializer: Serializer, uiSerializer: Serializer, tb: DomTestbed,
@ -299,7 +298,7 @@ class WorkerTestRootView extends TestRootView {
}
function templateRoot(pv: DomProtoView) {
return (<ReferenceCloneableTemplate>pv.cloneableTemplate).templateRoot;
return <Element>pv.cloneableTemplate;
}
function createSerializer(protoViewRefStore: RenderProtoViewRefStore,