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 {MapWrapper} from 'facade/collection';
import {BaseException} from 'facade/lang';
export class ContextWithVariableBindings { export class ContextWithVariableBindings {
parent:any; 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; varBindings:Map;
constructor(parent:any, varBindings:Map) { constructor(parent:any, varBindings:Map) {
@ -17,4 +18,21 @@ export class ContextWithVariableBindings {
get(name:string) { get(name:string) {
return MapWrapper.get(this.varBindings, name); 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 light Dom of the app element is not considered part of
// the angular application. Thus the context and lightDomInjector are // the angular application. Thus the context and lightDomInjector are
// empty. // 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]), }, [Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]),

View File

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

View File

@ -8,11 +8,12 @@ import {ProtoElementInjector, ElementInjector, PreBuiltObjects} from './element_
import {ElementBinder} from './element_binder'; import {ElementBinder} from './element_binder';
import {AnnotatedType} from './annotated_type'; import {AnnotatedType} from './annotated_type';
import {SetterFn} from 'reflection/types'; 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 {Injector} from 'di/di';
import {NgElement} from 'core/dom/element'; import {NgElement} from 'core/dom/element';
import {ViewPort} from './viewport'; import {ViewPort} from './viewport';
import {OnChange} from './interfaces'; import {OnChange} from './interfaces';
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
const NG_BINDING_CLASS = 'ng-binding'; const NG_BINDING_CLASS = 'ng-binding';
@ -33,9 +34,14 @@ export class View {
onChangeDispatcher:OnChangeDispatcher; onChangeDispatcher:OnChangeDispatcher;
componentChildViews: List<View>; componentChildViews: List<View>;
viewPorts: List<ViewPort>; 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, rootElementInjectors:List, textNodes:List, bindElements:List,
protoRecordRange:ProtoRecordRange, context) { protoRecordRange:ProtoRecordRange) {
this.proto = proto;
this.nodes = nodes; this.nodes = nodes;
this.elementInjectors = elementInjectors; this.elementInjectors = elementInjectors;
this.rootElementInjectors = rootElementInjectors; this.rootElementInjectors = rootElementInjectors;
@ -43,9 +49,120 @@ export class View {
this.textNodes = textNodes; this.textNodes = textNodes;
this.bindElements = bindElements; this.bindElements = bindElements;
this.recordRange = protoRecordRange.instantiate(this, MapWrapper.create()); this.recordRange = protoRecordRange.instantiate(this, MapWrapper.create());
this.recordRange.setContext(context);
this.componentChildViews = null; this.componentChildViews = null;
this.viewPorts = 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>) { onRecordChange(groupMemento, records:List<Record>) {
@ -108,12 +225,35 @@ export class View {
this.recordRange.addRange(childView.recordRange); this.recordRange.addRange(childView.recordRange);
} }
addViewPortChildView(childView: View) { _instantiateDirectives(
this.recordRange.addRange(childView.recordRange); 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) { _hydrateViewPorts(appInjector, hostElementInjector) {
childView.recordRange.remove(); 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; this.elementsWithBindingCount = 0;
} }
instantiate(context, lightDomAppInjector:Injector, // TODO(rado): hostElementInjector should be moved to hydrate phase.
hostElementInjector: ElementInjector, inPlace:boolean = false):View { // 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 clone = inPlace ? this.element : DOM.clone(this.element);
var elements; var elements;
if (clone instanceof TemplateElement) { if (clone instanceof TemplateElement) {
@ -157,7 +299,7 @@ export class ProtoView {
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors); var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
var textNodes = ProtoView._textNodes(elements, binders); var textNodes = ProtoView._textNodes(elements, binders);
var bindElements = ProtoView._bindElements(elements, binders); var bindElements = ProtoView._bindElements(elements, binders);
var shadowAppInjectors = ProtoView._createShadowAppInjectors(binders, lightDomAppInjector);
var viewNodes; var viewNodes;
if (clone instanceof TemplateElement) { if (clone instanceof TemplateElement) {
@ -165,14 +307,13 @@ export class ProtoView {
} else { } else {
viewNodes = [clone]; viewNodes = [clone];
} }
var view = new View(viewNodes, elementInjectors, rootElementInjectors, textNodes, var view = new View(this, viewNodes, elementInjectors, rootElementInjectors, textNodes,
bindElements, this.protoRecordRange, context); 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, ProtoView._instantiateChildComponentViews(view, elements, binders,
elementInjectors, shadowAppInjectors); elementInjectors);
return view; return view;
} }
@ -258,10 +399,8 @@ export class ProtoView {
return injectors; return injectors;
} }
static _instantiateDirectives( static _createPreBuiltObjects(view, injectors, elements, binders) {
view, elements:List, binders: List<ElementBinder>, injectors:List<ElementInjectors>, var preBuiltObjects = ListWrapper.createFixedSize(binders.length);
lightDomAppInjector: Injector, shadowDomAppInjectors:List<Injectors>,
hostElementInjector: ElementInjector) {
for (var i = 0; i < injectors.length; ++i) { for (var i = 0; i < injectors.length; ++i) {
var injector = injectors[i]; var injector = injectors[i];
if (injector != null) { if (injector != null) {
@ -271,16 +410,17 @@ export class ProtoView {
var viewPort = null; var viewPort = null;
if (isPresent(binder.templateDirective)) { if (isPresent(binder.templateDirective)) {
viewPort = new ViewPort(view, element, binder.nestedProtoView, injector); viewPort = new ViewPort(view, element, binder.nestedProtoView, injector);
viewPort.attach(lightDomAppInjector, hostElementInjector);
view.addViewPort(viewPort); view.addViewPort(viewPort);
} }
var preBuiltObjs = new PreBuiltObjects(view, ngElement, viewPort); preBuiltObjects[i] = new PreBuiltObjects(view, ngElement, viewPort);
injector.instantiateDirectives( } else {
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs); preBuiltObjects[i] = null;
} }
} }
return preBuiltObjects;
} }
static _rootElementInjectors(injectors) { static _rootElementInjectors(injectors) {
return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent)); return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent));
} }
@ -313,13 +453,12 @@ export class ProtoView {
} }
static _instantiateChildComponentViews(view: View, elements, binders, static _instantiateChildComponentViews(view: View, elements, binders,
injectors, shadowDomAppInjectors: List<Injector>) { injectors) {
for (var i = 0; i < binders.length; ++i) { for (var i = 0; i < binders.length; ++i) {
var binder = binders[i]; var binder = binders[i];
if (isPresent(binder.componentDirective)) { if (isPresent(binder.componentDirective)) {
var injector = injectors[i]; var injector = injectors[i];
var childView = binder.nestedProtoView.instantiate( var childView = binder.nestedProtoView.instantiate(injectors[i]);
injector.getComponent(), shadowDomAppInjectors[i], injector);
view.addComponentChildView(childView); view.addComponentChildView(childView);
var shadowRoot = elements[i].createShadowRoot(); var shadowRoot = elements[i].createShadowRoot();
ViewPort.moveViewNodesIntoParent(shadowRoot, childView); 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>, // Create a rootView as if the compiler encountered <rootcmp></rootcmp>,
// and the component template is already compiled into protoView. // and the component template is already compiled into protoView.
// Used for bootstrapping. // Used for bootstrapping.

View File

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

View File

@ -12,7 +12,7 @@ import {NgElement} from 'core/dom/element';
//TODO: vsavkin: use a spy object //TODO: vsavkin: use a spy object
class DummyView extends View { class DummyView extends View {
constructor() { 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 () { describe("instantiateDirectives", function () {
it("should instantiate directives that have no dependencies", function () { it("should instantiate directives that have no dependencies", function () {
var inj = injector([Directive]); 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 {TemplateConfig} from 'core/annotations/template_config';
import {ViewPort} from 'core/compiler/viewport'; import {ViewPort} from 'core/compiler/viewport';
import {MapWrapper} from 'facade/collection';
export function main() { export function main() {
describe('integration tests', function() { describe('integration tests', function() {
@ -27,7 +28,8 @@ export function main() {
var view, ctx, cd; var view, ctx, cd;
function createView(pv) { function createView(pv) {
ctx = new MyComp(); 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); cd = new ChangeDetector(view.recordRange);
} }
@ -79,7 +81,7 @@ export function main() {
}); });
it('should support template directives via `<template>` elements.', (done) => { 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); createView(pv);
cd.detectChanges(); cd.detectChanges();
@ -88,13 +90,13 @@ export function main() {
// 1 template + 2 copies. // 1 template + 2 copies.
expect(childNodesOfWrapper.length).toBe(3); expect(childNodesOfWrapper.length).toBe(3);
expect(childNodesOfWrapper[1].childNodes[0].nodeValue).toEqual('hello'); 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(); done();
}); });
}); });
it('should support template directives via `template` attribute.', (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); createView(pv);
cd.detectChanges(); cd.detectChanges();
@ -103,7 +105,7 @@ export function main() {
// 1 template + 2 copies. // 1 template + 2 copies.
expect(childNodesOfWrapper.length).toBe(3); expect(childNodesOfWrapper.length).toBe(3);
expect(childNodesOfWrapper[1].childNodes[0].nodeValue).toEqual('hello'); 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(); done();
}); });
}); });
@ -154,8 +156,8 @@ class ChildComp {
}) })
class SomeTemplate { class SomeTemplate {
constructor(viewPort: ViewPort) { constructor(viewPort: ViewPort) {
viewPort.create(); viewPort.create().setLocal('some-tmpl', 'hello');
viewPort.create(); viewPort.create().setLocal('some-tmpl', 'again');
} }
} }

View File

@ -78,7 +78,8 @@ export function main() {
function instantiateView(protoView) { function instantiateView(protoView) {
evalContext = new Context(); 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); changeDetector = new ChangeDetector(view.recordRange);
} }

View File

@ -20,14 +20,71 @@ export function main() {
describe('view', function() { describe('view', function() {
var parser, someComponentDirective, someTemplateDirective; var parser, someComponentDirective, someTemplateDirective;
function createView(protoView) {
var ctx = new MyEvaluationContext();
var view = protoView.instantiate(null);
view.hydrate(null, null, ctx);
return view;
}
beforeEach(() => { beforeEach(() => {
parser = new Parser(new Lexer()); parser = new Parser(new Lexer());
someComponentDirective = new DirectiveMetadataReader().annotatedType(SomeComponent); someComponentDirective = new DirectiveMetadataReader().annotatedType(SomeComponent);
someTemplateDirective = new DirectiveMetadataReader().annotatedType(SomeTemplate); 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) { function createCollectDomNodesTestCases(useTemplateElement:boolean) {
@ -37,7 +94,8 @@ export function main() {
it('should collect the root node in the ProtoView element', () => { it('should collect the root node in the ProtoView element', () => {
var pv = new ProtoView(templateAwareCreateElement('<div id="1"></div>'), new ProtoRecordRange()); 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.length).toBe(1);
expect(view.nodes[0].getAttribute('id')).toEqual('1'); expect(view.nodes[0].getAttribute('id')).toEqual('1');
}); });
@ -49,7 +107,8 @@ export function main() {
pv.bindElement(null); pv.bindElement(null);
pv.bindElementProperty('prop', parser.parseBinding('a').ast); 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.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0]); expect(view.bindElements[0]).toBe(view.nodes[0]);
}); });
@ -60,7 +119,8 @@ export function main() {
pv.bindElement(null); pv.bindElement(null);
pv.bindElementProperty('a', parser.parseBinding('b').ast); 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.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0].childNodes[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(0, parser.parseBinding('a').ast);
pv.bindTextNode(2, parser.parseBinding('b').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.length).toEqual(2);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]); expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]);
expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]); expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]);
@ -87,7 +148,8 @@ export function main() {
pv.bindElement(null); pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('b').ast); 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.length).toEqual(1);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]); expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]);
}); });
@ -99,14 +161,16 @@ export function main() {
it('should be supported.', () => { it('should be supported.', () => {
var template = createElement('<div></div>') var template = createElement('<div></div>')
var view = new ProtoView(template, new ProtoRecordRange()) 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); expect(view.nodes[0]).toBe(template);
}); });
it('should be off by default.', () => { it('should be off by default.', () => {
var template = createElement('<div></div>') var template = createElement('<div></div>')
var view = new ProtoView(template, new ProtoRecordRange()) 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); 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()); var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); 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.length).toBe(1);
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true); expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
}); });
@ -136,7 +201,8 @@ export function main() {
pv.bindElement(protoParent); pv.bindElement(protoParent);
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); 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.length).toBe(2);
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true); expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]); expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]);
@ -152,7 +218,8 @@ export function main() {
pv.bindElement(protoParent); pv.bindElement(protoParent);
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); 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.length).toBe(1);
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true); 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, 1, [SomeDirective]));
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective])); 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.length).toBe(2)
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true); expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
expect(view.rootElementInjectors[1].get(AnotherDirective) instanceof AnotherDirective).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; var ctx;
function createComponentWithSubPV(subProtoView) { function createComponentWithSubPV(subProtoView) {
@ -184,7 +252,9 @@ export function main() {
function createNestedView(protoView) { function createNestedView(protoView) {
ctx = new MyEvaluationContext(); 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', () => { it('should create shadow dom', () => {
@ -225,16 +295,30 @@ export function main() {
expect(subDecorator.service).toBe(comp.service); expect(subDecorator.service).toBe(comp.service);
expect(subDecorator.component).toBe(comp); expect(subDecorator.component).toBe(comp);
}); });
});
describe('recurse over child templateViews', () => { function expectViewHasNoDirectiveInstances(view) {
var ctx, view; view.elementInjectors.forEach((inj) => expect(inj.hasInstances()).toBe(false));
function createView(protoView) {
ctx = new MyEvaluationContext();
view = protoView.instantiate(ctx, null, null);
} }
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( var templateProtoView = new ProtoView(
createElement('<div id="1"></div>'), new ProtoRecordRange()); createElement('<div id="1"></div>'), new ProtoRecordRange());
var pv = new ProtoView(createElement('<someTmpl class="ng-binding"></someTmpl>'), 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.templateDirective = someTemplateDirective;
binder.nestedProtoView = templateProtoView; 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); var tmplComp = view.rootElementInjectors[0].get(SomeTemplate);
expect(tmplComp.viewPort).not.toBe(null); 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', () => { describe('react to record changes', () => {
var view, cd, ctx; var view, cd, ctx;
function createView(protoView) { function createViewAndChangeDetector(protoView) {
ctx = new MyEvaluationContext(); view = createView(protoView);
view = protoView.instantiate(ctx, null, null); ctx = view.context;
cd = new ChangeDetector(view.recordRange); cd = new ChangeDetector(view.recordRange);
} }
@ -263,7 +358,7 @@ export function main() {
new ProtoRecordRange()); new ProtoRecordRange());
pv.bindElement(null); pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('foo').ast); pv.bindTextNode(0, parser.parseBinding('foo').ast);
createView(pv); createViewAndChangeDetector(pv);
ctx.foo = 'buz'; ctx.foo = 'buz';
cd.detectChanges(); cd.detectChanges();
@ -275,7 +370,7 @@ export function main() {
new ProtoRecordRange()); new ProtoRecordRange());
pv.bindElement(null); pv.bindElement(null);
pv.bindElementProperty('id', parser.parseBinding('foo').ast); pv.bindElementProperty('id', parser.parseBinding('foo').ast);
createView(pv); createViewAndChangeDetector(pv);
ctx.foo = 'buz'; ctx.foo = 'buz';
cd.detectChanges(); cd.detectChanges();
@ -287,7 +382,7 @@ export function main() {
new ProtoRecordRange()); new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
pv.bindDirectiveProperty(0, parser.parseBinding('foo').ast, 'prop', reflector.setter('prop')); pv.bindDirectiveProperty(0, parser.parseBinding('foo').ast, 'prop', reflector.setter('prop'));
createView(pv); createViewAndChangeDetector(pv);
ctx.foo = 'buz'; ctx.foo = 'buz';
cd.detectChanges(); cd.detectChanges();
@ -301,7 +396,7 @@ export function main() {
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a')); pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a'));
pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b')); pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b'));
createView(pv); createViewAndChangeDetector(pv);
ctx.a = 100; ctx.a = 100;
ctx.b = 200; ctx.b = 200;
@ -318,7 +413,8 @@ export function main() {
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a')); pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a'));
pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b')); pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b'));
createView(pv); createViewAndChangeDetector(pv);
ctx.a = 0; ctx.a = 0;
ctx.b = 0; ctx.b = 0;
cd.detectChanges(); cd.detectChanges();
@ -342,13 +438,15 @@ export function main() {
it('should create the root component when instantiated', () => { it('should create the root component when instantiated', () => {
var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective); 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); expect(view.rootElementInjectors[0].get(SomeComponent)).not.toBe(null);
}); });
it('should inject the protoView into the shadowDom', () => { it('should inject the protoView into the shadowDom', () => {
var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective); 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'); expect(el.shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hi');
}); });
}); });

View File

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