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

@ -1,8 +1,9 @@
import {MapWrapper} from 'facade/collection';
import {BaseException} from 'facade/lang';
export class ContextWithVariableBindings {
parent:any;
/// varBindings are read-only. updating/adding keys is not supported.
/// varBindings' keys are read-only. adding/removing keys is not supported.
varBindings:Map;
constructor(parent:any, varBindings:Map) {
@ -17,4 +18,21 @@ export class ContextWithVariableBindings {
get(name:string) {
return MapWrapper.get(this.varBindings, name);
}
}
set(name:string, value) {
// TODO(rado): consider removing this check if we can guarantee this is not
// exposed to the public API.
if (this.hasBinding(name)) {
MapWrapper.set(this.varBindings, name, value);
} else {
throw new BaseException(
'VariableBindings do not support setting of new keys post-construction.');
}
}
clearValues() {
for (var [k, v] of MapWrapper.iterable(this.varBindings)) {
MapWrapper.set(this.varBindings, k, null);
}
}
}

View File

@ -0,0 +1,42 @@
import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'test_lib/test_lib';
import {BaseException, isBlank, isPresent} from 'facade/lang';
import {MapWrapper, ListWrapper} from 'facade/collection';
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
export function main() {
describe('ContextWithVariableBindings', () => {
var locals;
beforeEach(() => {
locals = new ContextWithVariableBindings(null,
MapWrapper.createFromPairs([['key', 'value'], ['nullKey', null]]));
});
it('should support getting values', () => {
expect(locals.get('key')).toBe('value');
var notPresentValue = locals.get('notPresent');
expect(isPresent(notPresentValue)).toBe(false);
});
it('should support checking if key is persent', () => {
expect(locals.hasBinding('key')).toBe(true);
expect(locals.hasBinding('nullKey')).toBe(true);
expect(locals.hasBinding('notPresent')).toBe(false);
});
it('should support setting persent keys', () => {
locals.set('key', 'bar');
expect(locals.get('key')).toBe('bar');
});
it('should not support setting keys that are not present already', () => {
expect(() => locals.set('notPresent', 'bar')).toThrowError();
});
it('should clearValues', () => {
locals.clearValues();
expect(locals.get('key')).toBe(null);
});
})
}

View File

@ -54,7 +54,9 @@ export function documentDependentBindings(appComponentType) {
// The light Dom of the app element is not considered part of
// the angular application. Thus the context and lightDomInjector are
// empty.
return appProtoView.instantiate(new Object(), injector, null, true);
var view = appProtoView.instantiate(null, true);
view.hydrate(injector, null, new Object());
return view;
});
}, [Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]),

View File

@ -459,6 +459,10 @@ export class ElementInjector extends TreeNode {
if (index == 9) return this._obj9;
throw new OutOfBoundsAccess(index);
}
hasInstances() {
return this._constructionCounter > 0;
}
}
class OutOfBoundsAccess extends Error {

View File

@ -8,11 +8,12 @@ import {ProtoElementInjector, ElementInjector, PreBuiltObjects} from './element_
import {ElementBinder} from './element_binder';
import {AnnotatedType} from './annotated_type';
import {SetterFn} from 'reflection/types';
import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang';
import {FIELD, IMPLEMENTS, int, isPresent, isBlank, BaseException} from 'facade/lang';
import {Injector} from 'di/di';
import {NgElement} from 'core/dom/element';
import {ViewPort} from './viewport';
import {OnChange} from './interfaces';
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
const NG_BINDING_CLASS = 'ng-binding';
@ -33,9 +34,14 @@ export class View {
onChangeDispatcher:OnChangeDispatcher;
componentChildViews: List<View>;
viewPorts: List<ViewPort>;
constructor(nodes:List<Node>, elementInjectors:List,
preBuiltObjects: List<PreBuiltObjects>;
proto: ProtoView;
context: Object;
_localBindings: Map;
constructor(proto:ProtoView, nodes:List<Node>, elementInjectors:List,
rootElementInjectors:List, textNodes:List, bindElements:List,
protoRecordRange:ProtoRecordRange, context) {
protoRecordRange:ProtoRecordRange) {
this.proto = proto;
this.nodes = nodes;
this.elementInjectors = elementInjectors;
this.rootElementInjectors = rootElementInjectors;
@ -43,9 +49,120 @@ export class View {
this.textNodes = textNodes;
this.bindElements = bindElements;
this.recordRange = protoRecordRange.instantiate(this, MapWrapper.create());
this.recordRange.setContext(context);
this.componentChildViews = null;
this.viewPorts = null;
this.preBuiltObjects = null;
this.context = null;
// used to persist the locals part of context inbetween hydrations.
this._localBindings = null;
if (isPresent(this.proto) && MapWrapper.size(this.proto.variableBindings) > 0) {
this._createLocalContext();
}
}
_createLocalContext() {
this._localBindings = MapWrapper.create();
for (var [ctxName, tmplName] of MapWrapper.iterable(this.proto.variableBindings)) {
MapWrapper.set(this._localBindings, tmplName, null);
}
}
setLocal(contextName: string, value) {
if (!this.hydrated()) throw new BaseException('Cannot set locals on dehydrated view.');
if (!MapWrapper.contains(this.proto.variableBindings, contextName)) {
throw new BaseException(
`Local binding ${contextName} not defined in the view template.`);
}
var templateName = MapWrapper.get(this.proto.variableBindings, contextName);
this.context.set(templateName, value);
}
hydrated() {
return isPresent(this.context);
}
_hydrateContext(newContext) {
if (isPresent(this._localBindings)) {
newContext = new ContextWithVariableBindings(newContext, this._localBindings);
}
this.recordRange.setContext(newContext);
this.context = newContext;
}
_dehydrateContext() {
if (isPresent(this._localBindings)) {
this.context.clearValues();
}
this.context = null;
}
/**
* A dehydrated view is a state of the view that allows it to be moved around
* the view tree, without incurring the cost of recreating the underlying
* injectors and watch records.
*
* A dehydrated view has the following properties:
*
* - all element injectors are empty.
* - all appInjectors are released.
* - all viewports are empty.
* - all context locals are set to null.
* - the view context is null.
*
* A call to hydrate/dehydrate does not attach/detach the view from the view
* tree.
*/
hydrate(appInjector: Injector, hostElementInjector: ElementInjector,
context: Object) {
if (isBlank(this.preBuiltObjects)) {
throw new BaseException('Cannot hydrate a view without pre-built objects.');
}
this._hydrateContext(context);
var shadowDomAppInjectors = View._createShadowDomInjectors(
this.proto, appInjector);
this._hydrateViewPorts(appInjector, hostElementInjector);
this._instantiateDirectives(appInjector, shadowDomAppInjectors);
this._hydrateChildComponentViews(appInjector, shadowDomAppInjectors);
}
dehydrate() {
// preserve the opposite order of the hydration process.
if (isPresent(this.componentChildViews)) {
for (var i = 0; i < this.componentChildViews.length; i++) {
this.componentChildViews[i].dehydrate();
}
}
for (var i = 0; i < this.elementInjectors.length; i++) {
this.elementInjectors[i].clearDirectives();
}
if (isPresent(this.viewPorts)) {
for (var i = 0; i < this.viewPorts.length; i++) {
this.viewPorts[i].dehydrate();
}
}
this._dehydrateContext();
}
static _createShadowDomInjectors(protoView, defaultInjector) {
var binders = protoView.elementBinders;
var shadowDomAppInjectors = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < binders.length; ++i) {
var componentDirective = binders[i].componentDirective;
if (isPresent(componentDirective)) {
var services = componentDirective.annotation.componentServices;
if (isPresent(services))
shadowDomAppInjectors[i] = defaultInjector.createChild(services);
else {
shadowDomAppInjectors[i] = defaultInjector;
}
} else {
shadowDomAppInjectors[i] = null;
}
}
return shadowDomAppInjectors;
}
onRecordChange(groupMemento, records:List<Record>) {
@ -108,12 +225,35 @@ export class View {
this.recordRange.addRange(childView.recordRange);
}
addViewPortChildView(childView: View) {
this.recordRange.addRange(childView.recordRange);
_instantiateDirectives(
lightDomAppInjector: Injector, shadowDomAppInjectors) {
for (var i = 0; i < this.elementInjectors.length; ++i) {
var injector = this.elementInjectors[i];
if (injector != null) {
injector.instantiateDirectives(
lightDomAppInjector, shadowDomAppInjectors[i], this.preBuiltObjects[i]);
}
}
}
removeViewPortChildView(childView: View) {
childView.recordRange.remove();
_hydrateViewPorts(appInjector, hostElementInjector) {
if (isBlank(this.viewPorts)) return;
for (var i = 0; i < this.viewPorts.length; i++) {
this.viewPorts[i].hydrate(appInjector, hostElementInjector);
}
}
_hydrateChildComponentViews(appInjector, shadowDomAppInjectors) {
var count = 0;
for (var i = 0; i < shadowDomAppInjectors.length; i++) {
var shadowDomInjector = shadowDomAppInjectors[i];
var injector = this.elementInjectors[i];
// replace with protoView.binder.
if (isPresent(shadowDomAppInjectors[i])) {
this.componentChildViews[count++].hydrate(shadowDomInjector,
injector, injector.getComponent());
}
}
}
}
@ -135,8 +275,10 @@ export class ProtoView {
this.elementsWithBindingCount = 0;
}
instantiate(context, lightDomAppInjector:Injector,
hostElementInjector: ElementInjector, inPlace:boolean = false):View {
// TODO(rado): hostElementInjector should be moved to hydrate phase.
// TODO(rado): inPlace is only used for bootstrapping, invastigate whether we can bootstrap without
// rootProtoView.
instantiate(hostElementInjector: ElementInjector, inPlace:boolean = false):View {
var clone = inPlace ? this.element : DOM.clone(this.element);
var elements;
if (clone instanceof TemplateElement) {
@ -157,7 +299,7 @@ export class ProtoView {
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
var textNodes = ProtoView._textNodes(elements, binders);
var bindElements = ProtoView._bindElements(elements, binders);
var shadowAppInjectors = ProtoView._createShadowAppInjectors(binders, lightDomAppInjector);
var viewNodes;
if (clone instanceof TemplateElement) {
@ -165,14 +307,13 @@ export class ProtoView {
} else {
viewNodes = [clone];
}
var view = new View(viewNodes, elementInjectors, rootElementInjectors, textNodes,
bindElements, this.protoRecordRange, context);
var view = new View(this, viewNodes, elementInjectors, rootElementInjectors, textNodes,
bindElements, this.protoRecordRange);
view.preBuiltObjects = ProtoView._createPreBuiltObjects(view, elementInjectors, elements, binders);
ProtoView._instantiateDirectives(
view, elements, binders, elementInjectors, lightDomAppInjector,
shadowAppInjectors, hostElementInjector);
ProtoView._instantiateChildComponentViews(view, elements, binders,
elementInjectors, shadowAppInjectors);
elementInjectors);
return view;
}
@ -258,10 +399,8 @@ export class ProtoView {
return injectors;
}
static _instantiateDirectives(
view, elements:List, binders: List<ElementBinder>, injectors:List<ElementInjectors>,
lightDomAppInjector: Injector, shadowDomAppInjectors:List<Injectors>,
hostElementInjector: ElementInjector) {
static _createPreBuiltObjects(view, injectors, elements, binders) {
var preBuiltObjects = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < injectors.length; ++i) {
var injector = injectors[i];
if (injector != null) {
@ -271,16 +410,17 @@ export class ProtoView {
var viewPort = null;
if (isPresent(binder.templateDirective)) {
viewPort = new ViewPort(view, element, binder.nestedProtoView, injector);
viewPort.attach(lightDomAppInjector, hostElementInjector);
view.addViewPort(viewPort);
}
var preBuiltObjs = new PreBuiltObjects(view, ngElement, viewPort);
injector.instantiateDirectives(
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs);
preBuiltObjects[i] = new PreBuiltObjects(view, ngElement, viewPort);
} else {
preBuiltObjects[i] = null;
}
}
return preBuiltObjects;
}
static _rootElementInjectors(injectors) {
return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent));
}
@ -313,13 +453,12 @@ export class ProtoView {
}
static _instantiateChildComponentViews(view: View, elements, binders,
injectors, shadowDomAppInjectors: List<Injector>) {
injectors) {
for (var i = 0; i < binders.length; ++i) {
var binder = binders[i];
if (isPresent(binder.componentDirective)) {
var injector = injectors[i];
var childView = binder.nestedProtoView.instantiate(
injector.getComponent(), shadowDomAppInjectors[i], injector);
var childView = binder.nestedProtoView.instantiate(injectors[i]);
view.addComponentChildView(childView);
var shadowRoot = elements[i].createShadowRoot();
ViewPort.moveViewNodesIntoParent(shadowRoot, childView);
@ -327,21 +466,6 @@ export class ProtoView {
}
}
static _createShadowAppInjectors(binders: List<ElementBinders>, lightDomAppInjector: Injector): List<Injectors> {
var injectors = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < binders.length; ++i) {
var componentDirective = binders[i].componentDirective;
if (isPresent(componentDirective)) {
var services = componentDirective.annotation.componentServices;
injectors[i] = isPresent(services) ?
lightDomAppInjector.createChild(services) : lightDomAppInjector;
} else {
injectors[i] = null;
}
}
return injectors;
}
// Create a rootView as if the compiler encountered <rootcmp></rootcmp>,
// and the component template is already compiled into protoView.
// Used for bootstrapping.

View File

@ -29,14 +29,17 @@ export class ViewPort {
this.hostElementInjector = null;
}
attach(appInjector: Injector, hostElementInjector: ElementInjector) {
hydrate(appInjector: Injector, hostElementInjector: ElementInjector) {
this.appInjector = appInjector;
this.hostElementInjector = hostElementInjector;
}
detach() {
dehydrate() {
this.appInjector = null;
this.hostElementInjector = null;
for (var i = 0; i < this._views.length; i++) {
this.remove(i);
}
}
get(index: number): View {
@ -52,19 +55,18 @@ export class ViewPort {
return ListWrapper.last(this._views[index - 1].nodes);
}
get detached() {
return isBlank(this.appInjector);
hydrated() {
return isPresent(this.appInjector);
}
// TODO(rado): profile and decide whether bounds checks should be added
// to the methods below.
create(atIndex=-1): View {
if (this.detached) throw new BaseException(
'Cannot create views on a detached view port');
// TODO(rado): replace curried defaultProtoView.instantiate(appInjector,
// hostElementInjector) with ViewFactory.
var newView = this.defaultProtoView.instantiate(
null, this.appInjector, this.hostElementInjector);
if (!this.hydrated()) throw new BaseException(
'Cannot create views on a dehydrated view port');
// TODO(rado): replace with viewFactory.
var newView = this.defaultProtoView.instantiate(this.hostElementInjector);
newView.hydrate(this.appInjector, this.hostElementInjector, this.parentView.context);
return this.insert(newView, atIndex);
}
@ -72,7 +74,7 @@ export class ViewPort {
if (atIndex == -1) atIndex = this._views.length;
ListWrapper.insert(this._views, atIndex, view);
ViewPort.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view);
this.parentView.addViewPortChildView(view);
this.parentView.recordRange.addRange(view.recordRange);
this._linkElementInjectors(view);
return view;
}
@ -82,7 +84,7 @@ export class ViewPort {
var removedView = this.get(atIndex);
ListWrapper.removeAt(this._views, atIndex);
ViewPort.removeViewNodesFromParent(this.templateElement.parentNode, removedView);
this.parentView.removeViewPortChildView(removedView);
removedView.recordRange.remove();
this._unlinkElementInjectors(removedView);
return removedView;
}

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);
});
});
});