feat(views): adds (de)hydration of views and template vars.

Dehydrated views are views that are structurally fixed, but their
directive instances and viewports are purged.

Support for local bindings is added to the view.
This commit is contained in:
Rado Kirov
2014-12-01 18:41:55 -08:00
parent 5c531f718e
commit 174613067c
11 changed files with 413 additions and 109 deletions

View File

@ -12,7 +12,7 @@ import {NgElement} from 'core/dom/element';
//TODO: vsavkin: use a spy object
class DummyView extends View {
constructor() {
super(null, null, null, null, null, new ProtoRecordRange(), null);
super(null, null, null, null, null, null, new ProtoRecordRange());
}
}
@ -150,6 +150,16 @@ export function main() {
});
});
describe("hasInstances", function () {
it("should be false when no directives are instantiated", function () {
expect(injector([]).hasInstances()).toBe(false);
});
it("should be true when directives are instantiated", function () {
expect(injector([Directive]).hasInstances()).toBe(true);
});
});
describe("instantiateDirectives", function () {
it("should instantiate directives that have no dependencies", function () {
var inj = injector([Directive]);

View File

@ -14,6 +14,7 @@ import {Decorator, Component, Template} from 'core/annotations/annotations';
import {TemplateConfig} from 'core/annotations/template_config';
import {ViewPort} from 'core/compiler/viewport';
import {MapWrapper} from 'facade/collection';
export function main() {
describe('integration tests', function() {
@ -27,7 +28,8 @@ export function main() {
var view, ctx, cd;
function createView(pv) {
ctx = new MyComp();
view = pv.instantiate(ctx, new Injector([]), null);
view = pv.instantiate(null);
view.hydrate(new Injector([]), null, ctx);
cd = new ChangeDetector(view.recordRange);
}
@ -79,7 +81,7 @@ export function main() {
});
it('should support template directives via `<template>` elements.', (done) => {
compiler.compile(MyComp, createElement('<div><template some-tmpl><copy-me>hello</copy-me></template></div>')).then((pv) => {
compiler.compile(MyComp, createElement('<div><template let-some-tmpl="greeting"><copy-me>{{greeting}}</copy-me></template></div>')).then((pv) => {
createView(pv);
cd.detectChanges();
@ -88,13 +90,13 @@ export function main() {
// 1 template + 2 copies.
expect(childNodesOfWrapper.length).toBe(3);
expect(childNodesOfWrapper[1].childNodes[0].nodeValue).toEqual('hello');
expect(childNodesOfWrapper[2].childNodes[0].nodeValue).toEqual('hello');
expect(childNodesOfWrapper[2].childNodes[0].nodeValue).toEqual('again');
done();
});
});
it('should support template directives via `template` attribute.', (done) => {
compiler.compile(MyComp, createElement('<div><copy-me template="some-tmpl">hello</copy-me></div>')).then((pv) => {
compiler.compile(MyComp, createElement('<div><copy-me template="some-tmpl #greeting">{{greeting}}</copy-me></div>')).then((pv) => {
createView(pv);
cd.detectChanges();
@ -103,7 +105,7 @@ export function main() {
// 1 template + 2 copies.
expect(childNodesOfWrapper.length).toBe(3);
expect(childNodesOfWrapper[1].childNodes[0].nodeValue).toEqual('hello');
expect(childNodesOfWrapper[2].childNodes[0].nodeValue).toEqual('hello');
expect(childNodesOfWrapper[2].childNodes[0].nodeValue).toEqual('again');
done();
});
});
@ -154,8 +156,8 @@ class ChildComp {
})
class SomeTemplate {
constructor(viewPort: ViewPort) {
viewPort.create();
viewPort.create();
viewPort.create().setLocal('some-tmpl', 'hello');
viewPort.create().setLocal('some-tmpl', 'again');
}
}

View File

@ -78,7 +78,8 @@ export function main() {
function instantiateView(protoView) {
evalContext = new Context();
view = protoView.instantiate(evalContext, new Injector([]), null);
view = protoView.instantiate(null);
view.hydrate(new Injector([]), null, evalContext);
changeDetector = new ChangeDetector(view.recordRange);
}

View File

@ -20,14 +20,71 @@ export function main() {
describe('view', function() {
var parser, someComponentDirective, someTemplateDirective;
function createView(protoView) {
var ctx = new MyEvaluationContext();
var view = protoView.instantiate(null);
view.hydrate(null, null, ctx);
return view;
}
beforeEach(() => {
parser = new Parser(new Lexer());
someComponentDirective = new DirectiveMetadataReader().annotatedType(SomeComponent);
someTemplateDirective = new DirectiveMetadataReader().annotatedType(SomeTemplate);
});
describe('instatiated from protoView', () => {
var view;
beforeEach(() => {
var pv = new ProtoView(createElement('<div id="1"></div>'), new ProtoRecordRange());
view = pv.instantiate(null);
});
describe('ProtoView.instantiate', function() {
it('should be dehydrated by default', () => {
expect(view.hydrated()).toBe(false);
});
it('should be able to be hydrated and dehydrated', () => {
var ctx = new Object();
view.hydrate(null, null, ctx);
expect(view.hydrated()).toBe(true);
view.dehydrate();
expect(view.hydrated()).toBe(false);
});
});
describe('with locals', function() {
var view;
beforeEach(() => {
var pv = new ProtoView(createElement('<div id="1"></div>'), new ProtoRecordRange());
pv.bindVariable('context-foo', 'template-foo');
view = createView(pv);
});
it('should support setting of declared locals', () => {
view.setLocal('context-foo', 'bar');
expect(view.context.get('template-foo')).toBe('bar');
});
it('should throw on undeclared locals', () => {
expect(() => view.setLocal('setMePlease', 'bar')).toThrowError();
});
it('when dehydrated should set locals to null', () => {
view.setLocal('context-foo', 'bar');
view.dehydrate();
view.hydrate(null, null, new Object());
expect(view.context.get('template-foo')).toBe(null);
});
it('should throw when trying to set on dehydrated view', () => {
view.dehydrate();
expect(() => view.setLocal('context-foo', 'bar')).toThrowError();
});
});
describe('instatiated and hydrated', function() {
function createCollectDomNodesTestCases(useTemplateElement:boolean) {
@ -37,7 +94,8 @@ export function main() {
it('should collect the root node in the ProtoView element', () => {
var pv = new ProtoView(templateAwareCreateElement('<div id="1"></div>'), new ProtoRecordRange());
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.nodes.length).toBe(1);
expect(view.nodes[0].getAttribute('id')).toEqual('1');
});
@ -49,7 +107,8 @@ export function main() {
pv.bindElement(null);
pv.bindElementProperty('prop', parser.parseBinding('a').ast);
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.bindElements.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0]);
});
@ -60,7 +119,8 @@ export function main() {
pv.bindElement(null);
pv.bindElementProperty('a', parser.parseBinding('b').ast);
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.bindElements.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0].childNodes[1]);
});
@ -75,7 +135,8 @@ export function main() {
pv.bindTextNode(0, parser.parseBinding('a').ast);
pv.bindTextNode(2, parser.parseBinding('b').ast);
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.textNodes.length).toEqual(2);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]);
expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]);
@ -87,7 +148,8 @@ export function main() {
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('b').ast);
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.textNodes.length).toEqual(1);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]);
});
@ -99,14 +161,16 @@ export function main() {
it('should be supported.', () => {
var template = createElement('<div></div>')
var view = new ProtoView(template, new ProtoRecordRange())
.instantiate(null, null, null, true);
.instantiate(null, true);
view.hydrate(null, null, null);
expect(view.nodes[0]).toBe(template);
});
it('should be off by default.', () => {
var template = createElement('<div></div>')
var view = new ProtoView(template, new ProtoRecordRange())
.instantiate(null, null, null);
.instantiate(null);
view.hydrate(null, null, null);
expect(view.nodes[0]).not.toBe(template);
});
});
@ -124,7 +188,8 @@ export function main() {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.elementInjectors.length).toBe(1);
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
});
@ -136,7 +201,8 @@ export function main() {
pv.bindElement(protoParent);
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.elementInjectors.length).toBe(2);
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]);
@ -152,7 +218,8 @@ export function main() {
pv.bindElement(protoParent);
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.rootElementInjectors.length).toBe(1);
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
});
@ -163,7 +230,8 @@ export function main() {
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective]));
var view = pv.instantiate(null, null, null);
var view = pv.instantiate(null);
view.hydrate(null, null, null);
expect(view.rootElementInjectors.length).toBe(2)
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
expect(view.rootElementInjectors[1].get(AnotherDirective) instanceof AnotherDirective).toBe(true);
@ -171,7 +239,7 @@ export function main() {
});
describe('recurse over child component views', () => {
describe('with component views', () => {
var ctx;
function createComponentWithSubPV(subProtoView) {
@ -184,7 +252,9 @@ export function main() {
function createNestedView(protoView) {
ctx = new MyEvaluationContext();
return protoView.instantiate(ctx, new Injector([]), null);
var view = protoView.instantiate(null);
view.hydrate(new Injector([]), null, ctx);
return view;
}
it('should create shadow dom', () => {
@ -225,16 +295,30 @@ export function main() {
expect(subDecorator.service).toBe(comp.service);
expect(subDecorator.component).toBe(comp);
});
});
describe('recurse over child templateViews', () => {
var ctx, view;
function createView(protoView) {
ctx = new MyEvaluationContext();
view = protoView.instantiate(ctx, null, null);
function expectViewHasNoDirectiveInstances(view) {
view.elementInjectors.forEach((inj) => expect(inj.hasInstances()).toBe(false));
}
it('should create a viewPort for the template directive', () => {
it('dehydration should dehydrate child component views too', () => {
var subpv = new ProtoView(
createElement('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoRecordRange());
subpv.bindElement(
new ProtoElementInjector(null, 0, [ServiceDependentDecorator]));
var pv = createComponentWithSubPV(subpv);
var view = createNestedView(pv);
view.dehydrate();
expect(view.hydrated()).toBe(false);
expectViewHasNoDirectiveInstances(view);
view.componentChildViews.forEach(
(view) => expectViewHasNoDirectiveInstances(view));
});
});
describe('with template views', () => {
function createViewWithTemplate() {
var templateProtoView = new ProtoView(
createElement('<div id="1"></div>'), new ProtoRecordRange());
var pv = new ProtoView(createElement('<someTmpl class="ng-binding"></someTmpl>'), new ProtoRecordRange());
@ -242,19 +326,30 @@ export function main() {
binder.templateDirective = someTemplateDirective;
binder.nestedProtoView = templateProtoView;
createView(pv);
return createView(pv);
}
it('should create a viewPort for the template directive', () => {
var view = createViewWithTemplate();
var tmplComp = view.rootElementInjectors[0].get(SomeTemplate);
expect(tmplComp.viewPort).not.toBe(null);
});
it('dehydration should dehydrate viewports', () => {
var view = createViewWithTemplate();
var tmplComp = view.rootElementInjectors[0].get(SomeTemplate);
expect(tmplComp.viewPort.hydrated()).toBe(false);
});
});
describe('react to record changes', () => {
var view, cd, ctx;
function createView(protoView) {
ctx = new MyEvaluationContext();
view = protoView.instantiate(ctx, null, null);
function createViewAndChangeDetector(protoView) {
view = createView(protoView);
ctx = view.context;
cd = new ChangeDetector(view.recordRange);
}
@ -263,7 +358,7 @@ export function main() {
new ProtoRecordRange());
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('foo').ast);
createView(pv);
createViewAndChangeDetector(pv);
ctx.foo = 'buz';
cd.detectChanges();
@ -275,7 +370,7 @@ export function main() {
new ProtoRecordRange());
pv.bindElement(null);
pv.bindElementProperty('id', parser.parseBinding('foo').ast);
createView(pv);
createViewAndChangeDetector(pv);
ctx.foo = 'buz';
cd.detectChanges();
@ -286,8 +381,8 @@ export function main() {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
pv.bindDirectiveProperty( 0, parser.parseBinding('foo').ast, 'prop', reflector.setter('prop'));
createView(pv);
pv.bindDirectiveProperty(0, parser.parseBinding('foo').ast, 'prop', reflector.setter('prop'));
createViewAndChangeDetector(pv);
ctx.foo = 'buz';
cd.detectChanges();
@ -301,7 +396,7 @@ export function main() {
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a'));
pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b'));
createView(pv);
createViewAndChangeDetector(pv);
ctx.a = 100;
ctx.b = 200;
@ -318,7 +413,8 @@ export function main() {
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a'));
pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b'));
createView(pv);
createViewAndChangeDetector(pv);
ctx.a = 0;
ctx.b = 0;
cd.detectChanges();
@ -342,13 +438,15 @@ export function main() {
it('should create the root component when instantiated', () => {
var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective);
var view = rootProtoView.instantiate(null, new Injector([]), null, true);
var view = rootProtoView.instantiate(null, true);
view.hydrate(new Injector([]), null, null);
expect(view.rootElementInjectors[0].get(SomeComponent)).not.toBe(null);
});
it('should inject the protoView into the shadowDom', () => {
var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective);
rootProtoView.instantiate(null, new Injector([]), null, true);
var view = rootProtoView.instantiate(null, true);
view.hydrate(new Injector([]), null, null);
expect(el.shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hi');
});
});

View File

@ -14,13 +14,13 @@ function createElement(html) {
}
function createView(nodes) {
return new View(nodes, [], [], [], [], new ProtoRecordRange(), null);
return new View(null, nodes, [], [], [], [], new ProtoRecordRange());
}
export function main() {
describe('viewport', () => {
var viewPort, parentView, protoView, dom, customViewWithOneNode,
customViewWithTwoNodes, elementInjector;
customViewWithTwoNodes, elementInjector;
beforeEach(() => {
dom = createElement(`<div><stuff></stuff><div insert-after-me></div><stuff></stuff></div>`);
@ -33,13 +33,13 @@ export function main() {
customViewWithTwoNodes = createView([createElement('<div>one</div>'), createElement('<div>two</div>')]);
});
describe('when detached', () => {
describe('when dehydrated', () => {
it('should throw if create is called', () => {
expect(() => viewPort.create()).toThrowError();
});
});
describe('when attached', () => {
describe('when hydrated', () => {
function textInViewPort() {
var out = '';
// skipping starting filler, insert-me and final filler.
@ -51,7 +51,7 @@ export function main() {
}
beforeEach(() => {
viewPort.attach(new Injector([]), null);
viewPort.hydrate(new Injector([]), null);
var fillerView = createView([createElement('<filler>filler</filler>')]);
viewPort.insert(fillerView);
});
@ -118,28 +118,29 @@ export function main() {
var fancyView;
beforeEach(() => {
var parser = new Parser(new Lexer());
viewPort.attach(new Injector([]), null);
viewPort.hydrate(new Injector([]), null);
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}</div>'),
new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
pv.bindTextNode(0, parser.parseBinding('foo').ast);
fancyView = pv.instantiate(new Object(), null, null);
fancyView = pv.instantiate(null);
});
it('attaching should update rootElementInjectors and parent RR', () => {
it('hydrating should update rootElementInjectors and parent RR', () => {
viewPort.insert(fancyView);
ListWrapper.forEach(fancyView.rootElementInjectors, (inj) =>
expect(inj.parent).toBe(elementInjector));
expect(parentView.recordRange.findFirstEnabledRecord()).not.toBe(null);
});
it('detaching should update rootElementInjectors and parent RR', () => {
it('dehydrating should update rootElementInjectors and parent RR', () => {
viewPort.insert(fancyView);
viewPort.remove();
ListWrapper.forEach(fancyView.rootElementInjectors, (inj) =>
expect(inj.parent).toBe(null));
expect(parentView.recordRange.findFirstEnabledRecord()).toBe(null);
expect(viewPort.length).toBe(0);
});
});
});