diff --git a/modules/change_detection/src/parser/context_with_variable_bindings.js b/modules/change_detection/src/parser/context_with_variable_bindings.js index f390aa42d2..2f4b630e81 100644 --- a/modules/change_detection/src/parser/context_with_variable_bindings.js +++ b/modules/change_detection/src/parser/context_with_variable_bindings.js @@ -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); } -} \ No newline at end of file + + 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); + } + } +} diff --git a/modules/change_detection/test/parser/context_with_variable_bindings_spec.js b/modules/change_detection/test/parser/context_with_variable_bindings_spec.js new file mode 100644 index 0000000000..fdf5e90de1 --- /dev/null +++ b/modules/change_detection/test/parser/context_with_variable_bindings_spec.js @@ -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); + }); + }) +} + diff --git a/modules/core/src/application.js b/modules/core/src/application.js index 68813cc18f..30ce233e7b 100644 --- a/modules/core/src/application.js +++ b/modules/core/src/application.js @@ -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]), diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index c1043106dd..5dab9c8a67 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -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 { diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index 544f2dd9db..df3df0016a 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -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; viewPorts: List; - constructor(nodes:List, elementInjectors:List, + preBuiltObjects: List; + proto: ProtoView; + context: Object; + _localBindings: Map; + constructor(proto:ProtoView, nodes:List, 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) { @@ -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, injectors:List, - lightDomAppInjector: Injector, shadowDomAppInjectors:List, - 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) { + 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, lightDomAppInjector: Injector): List { - 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 , // and the component template is already compiled into protoView. // Used for bootstrapping. diff --git a/modules/core/src/compiler/viewport.js b/modules/core/src/compiler/viewport.js index cb1bf550d5..fa6ac0a529 100644 --- a/modules/core/src/compiler/viewport.js +++ b/modules/core/src/compiler/viewport.js @@ -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; } diff --git a/modules/core/test/compiler/element_injector_spec.js b/modules/core/test/compiler/element_injector_spec.js index a898acb036..8ebf091c4f 100644 --- a/modules/core/test/compiler/element_injector_spec.js +++ b/modules/core/test/compiler/element_injector_spec.js @@ -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]); diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js index 7901e1d083..3588aa9ad4 100644 --- a/modules/core/test/compiler/integration_spec.js +++ b/modules/core/test/compiler/integration_spec.js @@ -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 `