diff --git a/modules/core/src/application.js b/modules/core/src/application.js index 9eba479b38..2c0ab60192 100644 --- a/modules/core/src/application.js +++ b/modules/core/src/application.js @@ -54,7 +54,7 @@ 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. - var view = appProtoView.instantiate(null, true); + var view = appProtoView.instantiate(null); view.hydrate(injector, null, new Object()); return view; }); diff --git a/modules/core/src/compiler/pipeline/element_binder_builder.js b/modules/core/src/compiler/pipeline/element_binder_builder.js index 829f6aa6ac..6b657cd7bd 100644 --- a/modules/core/src/compiler/pipeline/element_binder_builder.js +++ b/modules/core/src/compiler/pipeline/element_binder_builder.js @@ -1,5 +1,5 @@ import {int, isPresent, isBlank, Type, BaseException, stringify} from 'facade/lang'; -import {Element} from 'facade/dom'; +import {Element, DOM} from 'facade/dom'; import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'facade/collection'; import {reflector} from 'reflection/reflection'; @@ -74,7 +74,9 @@ export class ElementBinderBuilder extends CompileStep { _bindElementProperties(protoView, compileElement) { MapWrapper.forEach(compileElement.propertyBindings, (expression, property) => { - protoView.bindElementProperty(property, expression); + if (DOM.hasProperty(compileElement.element, property)) { + protoView.bindElementProperty(expression.ast, property, reflector.setter(property)); + } }); } diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index 3c6ab14f50..40bbb56dae 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -16,6 +16,9 @@ import {OnChange} from './interfaces'; import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings'; const NG_BINDING_CLASS = 'ng-binding'; +const NG_BINDING_CLASS_SELECTOR = '.ng-binding'; +// TODO(tbosch): Cannot use `const` because of Dart. +var NO_FORMATTERS = MapWrapper.create(); /** * Const of making objects: http://jsperf.com/instantiate-size-of-object @@ -36,34 +39,36 @@ export class View { preBuiltObjects: List; proto: ProtoView; context: Object; - _localBindings: Map; - constructor(proto:ProtoView, nodes:List, elementInjectors:List, - rootElementInjectors:List, textNodes:List, bindElements:List, - protoRecordRange:ProtoRecordRange) { + contextWithLocals:ContextWithVariableBindings; + constructor(proto:ProtoView, nodes:List, protoRecordRange:ProtoRecordRange, protoContextLocals:Map) { this.proto = proto; this.nodes = nodes; - this.elementInjectors = elementInjectors; - this.rootElementInjectors = rootElementInjectors; - this.textNodes = textNodes; - this.bindElements = bindElements; - this.recordRange = protoRecordRange.instantiate(this, MapWrapper.create()); + this.recordRange = protoRecordRange.instantiate(this, NO_FORMATTERS); + this.elementInjectors = null; + this.rootElementInjectors = null; + this.textNodes = null; + this.bindElements = null; 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(); + // contextWithLocals + if (MapWrapper.size(protoContextLocals) > 0) { + this.contextWithLocals = new ContextWithVariableBindings(null, MapWrapper.clone(protoContextLocals)); + } else { + this.contextWithLocals = null; } } - _createLocalContext() { - this._localBindings = MapWrapper.create(); - for (var [ctxName, tmplName] of MapWrapper.iterable(this.proto.variableBindings)) { - MapWrapper.set(this._localBindings, tmplName, null); - } + init(elementInjectors:List, rootElementInjectors:List, textNodes: List, bindElements:List, viewPorts:List, preBuiltObjects:List, componentChildViews:List) { + this.elementInjectors = elementInjectors; + this.rootElementInjectors = rootElementInjectors; + this.textNodes = textNodes; + this.bindElements = bindElements; + this.viewPorts = viewPorts; + this.preBuiltObjects = preBuiltObjects; + this.componentChildViews = componentChildViews; } setLocal(contextName: string, value) { @@ -81,16 +86,21 @@ export class View { } _hydrateContext(newContext) { - if (isPresent(this._localBindings)) { - newContext = new ContextWithVariableBindings(newContext, this._localBindings); + if (isPresent(this.contextWithLocals)) { + this.contextWithLocals.parent = newContext; + this.context = this.contextWithLocals; + } else { + this.context = newContext; } - this.recordRange.setContext(newContext); - this.context = newContext; + // TODO(tbosch): if we have a contextWithLocals we actually only need to + // set the contextWithLocals once. Would it be faster to always use a contextWithLocals + // even if we don't have locals and not update the recordRange here? + this.recordRange.setContext(this.context); } _dehydrateContext() { - if (isPresent(this._localBindings)) { - this.context.clearValues(); + if (isPresent(this.contextWithLocals)) { + this.contextWithLocals.clearValues(); } this.context = null; } @@ -113,54 +123,67 @@ export class View { */ hydrate(appInjector: Injector, hostElementInjector: ElementInjector, context: Object) { - if (isBlank(this.preBuiltObjects)) { - throw new BaseException('Cannot hydrate a view without pre-built objects.'); - } + if (this.hydrated()) throw new BaseException('The view is already hydrated.'); this._hydrateContext(context); - var shadowDomAppInjectors = View._createShadowDomInjectors( - this.proto, appInjector); + // viewPorts + for (var i = 0; i < this.viewPorts.length; i++) { + this.viewPorts[i].hydrate(appInjector, hostElementInjector); + } - this._hydrateViewPorts(appInjector, hostElementInjector); - this._instantiateDirectives(appInjector, shadowDomAppInjectors); - this._hydrateChildComponentViews(appInjector, shadowDomAppInjectors); + var binders = this.proto.elementBinders; + var componentChildViewIndex = 0; + for (var i = 0; i < binders.length; ++i) { + var componentDirective = binders[i].componentDirective; + var shadowDomAppInjector = null; + + // shadowDomAppInjector + if (isPresent(componentDirective)) { + var services = componentDirective.annotation.componentServices; + if (isPresent(services)) + shadowDomAppInjector = appInjector.createChild(services); + else { + shadowDomAppInjector = appInjector; + } + } else { + shadowDomAppInjector = null; + } + + // elementInjectors + var elementInjector = this.elementInjectors[i]; + if (isPresent(elementInjector)) { + elementInjector.instantiateDirectives(appInjector, shadowDomAppInjector, this.preBuiltObjects[i]); + } + + // componentChildViews + if (isPresent(shadowDomAppInjector)) { + this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector, + elementInjector, elementInjector.getComponent()); + } + } } 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(); - } + // Note: preserve the opposite order of the hydration process. + + // componentChildViews + for (var i = 0; i < this.componentChildViews.length; i++) { + this.componentChildViews[i].dehydrate(); } + + // elementInjectors for (var i = 0; i < this.elementInjectors.length; i++) { this.elementInjectors[i].clearDirectives(); } + + // viewPorts 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; + this._dehydrateContext(); } onRecordChange(groupMemento, records:List) { @@ -211,48 +234,6 @@ export class View { } return changes; } - - addViewPort(viewPort: ViewPort) { - if (isBlank(this.viewPorts)) this.viewPorts = []; - ListWrapper.push(this.viewPorts, viewPort); - } - - addComponentChildView(childView: View) { - if (isBlank(this.componentChildViews)) this.componentChildViews = []; - ListWrapper.push(this.componentChildViews, childView); - 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]); - } - } - } - - _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()); - } - } - } } export class ProtoView { @@ -260,64 +241,129 @@ export class ProtoView { elementBinders:List; protoRecordRange:ProtoRecordRange; variableBindings: Map; + protoContextLocals:Map; textNodesWithBindingCount:int; elementsWithBindingCount:int; + instantiateInPlace:boolean; + rootBindingOffset:int; + isTemplateElement:boolean; constructor( template:Element, protoRecordRange:ProtoRecordRange) { this.element = template; this.elementBinders = []; this.variableBindings = MapWrapper.create(); + this.protoContextLocals = MapWrapper.create(); this.protoRecordRange = protoRecordRange; this.textNodesWithBindingCount = 0; this.elementsWithBindingCount = 0; + this.instantiateInPlace = false; + if (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) { + this.rootBindingOffset = 1; + } else { + this.rootBindingOffset = 0; + } + this.isTemplateElement = this.element instanceof TemplateElement; } // 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) { - elements = ListWrapper.clone(DOM.querySelectorAll(clone.content, `.${NG_BINDING_CLASS}`)); + instantiate(hostElementInjector: ElementInjector):View { + var rootElementClone = this.instantiateInPlace ? this.element : DOM.clone(this.element); + var elementsWithBindings; + if (this.isTemplateElement) { + elementsWithBindings = DOM.querySelectorAll(rootElementClone.content, NG_BINDING_CLASS_SELECTOR); } else { - elements = ListWrapper.clone(DOM.getElementsByClassName(clone, NG_BINDING_CLASS)); + elementsWithBindings = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS); } - if (DOM.hasClass(clone, NG_BINDING_CLASS)) { - ListWrapper.insert(elements, 0, clone); - } - var binders = this.elementBinders; - - /** - * TODO: vsavkin: benchmark - * If this performs poorly, the seven loops can be collapsed into one. - */ - var elementInjectors = ProtoView._createElementInjectors(elements, binders, hostElementInjector); - var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors); - var textNodes = ProtoView._textNodes(elements, binders); - var bindElements = ProtoView._bindElements(elements, binders); - var viewNodes; - - if (clone instanceof TemplateElement) { - viewNodes = ListWrapper.clone(clone.content.childNodes); + if (this.isTemplateElement) { + var childNodes = rootElementClone.content.childNodes; + // Note: An explicit loop is the fastes way to convert a DOM array into a JS array! + viewNodes = ListWrapper.createFixedSize(childNodes.length); + for (var i=0; i isPresent(inj) && isBlank(inj.parent)); - } - - static _textNodes(elements, binders) { - var textNodes = []; - for (var i = 0; i < binders.length; ++i) { - ProtoView._collectTextNodes(textNodes, elements[i], - binders[i].textNodeIndices); - } - return textNodes; - } - - static _bindElements(elements, binders):List { - var bindElements = []; - for (var i = 0; i < binders.length; ++i) { - if (binders[i].hasElementPropertyBindings) ListWrapper.push( - bindElements, elements[i]); - } - return bindElements; - } - - static _collectTextNodes(allTextNodes, element, indices) { - if (isPresent(indices)) { - var childNodes = DOM.templateAwareRoot(element).childNodes; - for (var i = 0; i < indices.length; ++i) { - ListWrapper.push(allTextNodes, childNodes[indices[i]]); - } - } - } - - static _instantiateChildComponentViews(view: View, elements, binders, - 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(injectors[i]); - view.addComponentChildView(childView); - var shadowRoot = elements[i].createShadowRoot(); - ViewPort.moveViewNodesIntoParent(shadowRoot, childView); - } - } - } - // Create a rootView as if the compiler encountered , // and the component template is already compiled into protoView. // Used for bootstrapping. static createRootProtoView(protoView: ProtoView, insertionElement, rootComponentAnnotatedType: AnnotatedType): ProtoView { + DOM.addClass(insertionElement, 'ng-binding'); var rootProtoView = new ProtoView(insertionElement, new ProtoRecordRange()); + rootProtoView.instantiateInPlace = true; var binder = rootProtoView.bindElement( new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true)); binder.componentDirective = rootComponentAnnotatedType; binder.nestedProtoView = protoView; - DOM.addClass(insertionElement, 'ng-binding'); return rootProtoView; } } export class ElementPropertyMemento { _elementIndex:int; - _propertyName:string; - constructor(elementIndex:int, propertyName:string) { + _setterName:string; + _setter:SetterFn; + constructor(elementIndex:int, setterName:string, setter:SetterFn) { this._elementIndex = elementIndex; - this._propertyName = propertyName; + this._setterName = setterName; + this._setter = setter; } invoke(record:Record, bindElements:List) { var element:Element = bindElements[this._elementIndex]; - DOM.setProperty(element, this._propertyName, record.currentValue); + this._setter(element, record.currentValue); } } diff --git a/modules/core/test/compiler/element_injector_spec.js b/modules/core/test/compiler/element_injector_spec.js index 8ebf091c4f..96023b5cbb 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, null, new ProtoRecordRange()); + super(null, null, new ProtoRecordRange(), MapWrapper.create()); } } diff --git a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js index 5e53e5a935..ef2dae60b1 100644 --- a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js +++ b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js @@ -32,12 +32,6 @@ export function main() { var parser = new Parser(new Lexer()); return new CompilePipeline([ new MockStep((parent, current, control) => { - if (isPresent(current.element.getAttribute('viewroot'))) { - current.isViewRoot = true; - current.inheritedProtoView = new ProtoView(current.element, new ProtoRecordRange()); - } else if (isPresent(parent)) { - current.inheritedProtoView = parent.inheritedProtoView; - } var hasBinding = false; if (isPresent(current.element.getAttribute('text-binding'))) { MapWrapper.forEach(textNodeBindings, (v,k) => { @@ -72,6 +66,12 @@ export function main() { current.hasBindings = true; DOM.addClass(current.element, 'ng-binding'); } + if (isPresent(current.element.getAttribute('viewroot'))) { + current.isViewRoot = true; + current.inheritedProtoView = new ProtoView(current.element, new ProtoRecordRange()); + } else if (isPresent(parent)) { + current.inheritedProtoView = parent.inheritedProtoView; + } }), new ElementBinderBuilder() ]); } @@ -159,22 +159,22 @@ export function main() { it('should bind element properties', () => { var propertyBindings = MapWrapper.createFromStringMap({ - 'elprop1': 'prop1', - 'elprop2': 'prop2' + 'value': 'prop1', + 'hidden': 'prop2' }); var pipeline = createPipeline({propertyBindings: propertyBindings}); - var results = pipeline.process(createElement('
')); + var results = pipeline.process(createElement('')); var pv = results[0].inheritedProtoView; expect(pv.elementBinders[0].hasElementPropertyBindings).toBe(true); instantiateView(pv); evalContext.prop1 = 'a'; - evalContext.prop2 = 'b'; + evalContext.prop2 = false; changeDetector.detectChanges(); - expect(DOM.getProperty(view.nodes[0], 'elprop1')).toEqual('a'); - expect(DOM.getProperty(view.nodes[0], 'elprop2')).toEqual('b'); + expect(view.nodes[0].value).toEqual('a'); + expect(view.nodes[0].hidden).toEqual(false); }); it('should bind events', () => { diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index a33383923a..50e5b1c26b 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -105,7 +105,7 @@ export function main() { it('should collect property bindings on the root element if it has the ng-binding class', () => { var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoRecordRange()); pv.bindElement(null); - pv.bindElementProperty('prop', parser.parseBinding('a')); + pv.bindElementProperty(parser.parseBinding('a').ast, 'prop', reflector.setter('prop')); var view = pv.instantiate(null); view.hydrate(null, null, null); @@ -117,7 +117,7 @@ export function main() { var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoRecordRange()); pv.bindElement(null); - pv.bindElementProperty('a', parser.parseBinding('b')); + pv.bindElementProperty(parser.parseBinding('b').ast, 'a', reflector.setter('a')); var view = pv.instantiate(null); view.hydrate(null, null, null); @@ -159,9 +159,10 @@ export function main() { describe('inplace instantiation', () => { it('should be supported.', () => { - var template = createElement('
') - var view = new ProtoView(template, new ProtoRecordRange()) - .instantiate(null, true); + var template = createElement('
'); + var pv = new ProtoView(template, new ProtoRecordRange()); + pv.instantiateInPlace = true; + var view = pv.instantiate(null); view.hydrate(null, null, null); expect(view.nodes[0]).toBe(template); }); @@ -369,7 +370,7 @@ export function main() { var pv = new ProtoView(createElement('
'), new ProtoRecordRange()); pv.bindElement(null); - pv.bindElementProperty('id', parser.parseBinding('foo')); + pv.bindElementProperty(parser.parseBinding('foo').ast, 'id', reflector.setter('id')); createViewAndChangeDetector(pv); ctx.foo = 'buz'; @@ -437,14 +438,14 @@ export function main() { it('should create the root component when instantiated', () => { var rootProtoView = ProtoView.createRootProtoView(pv, el, someComponentDirective); - var view = rootProtoView.instantiate(null, true); + var view = rootProtoView.instantiate(null); 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); - var view = rootProtoView.instantiate(null, true); + var view = rootProtoView.instantiate(null); view.hydrate(new Injector([]), null, null); expect(el.shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hi'); }); diff --git a/modules/core/test/compiler/viewport_spec.js b/modules/core/test/compiler/viewport_spec.js index 773fcbdee2..29763ba1f4 100644 --- a/modules/core/test/compiler/viewport_spec.js +++ b/modules/core/test/compiler/viewport_spec.js @@ -14,7 +14,9 @@ function createElement(html) { } function createView(nodes) { - return new View(null, nodes, [], [], [], [], new ProtoRecordRange()); + var view = new View(null, nodes, new ProtoRecordRange(), MapWrapper.create()); + view.init([], [], [], [], [], [], []); + return view; } export function main() { diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index a9093ac592..d39ebd5099 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -32,6 +32,7 @@ class IterableMap extends IterableBase { class MapWrapper { static HashMap create() => new HashMap(); + static HashMap clone(Map m) => new HashMap.from(m); static HashMap createFromStringMap(m) => m; static HashMap createFromPairs(List pairs) { return pairs.fold({}, (m, p){ diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 3df3e3b375..b6e7807b8c 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -6,6 +6,7 @@ export var Set = window.Set; export class MapWrapper { static create():Map { return new Map(); } + static clone(m:Map):Map { return new Map(m); } static createFromStringMap(stringMap):Map { var result = MapWrapper.create(); for (var prop in stringMap) { @@ -63,8 +64,8 @@ export class ListWrapper { static createFixedSize(size):List { return new List(size); } static get(m, k) { return m[k]; } static set(m, k, v) { m[k] = v; } - static clone(array) { - return Array.prototype.slice.call(array, 0); + static clone(array:List) { + return array.slice(0); } static map(array, fn) { return array.map(fn); diff --git a/modules/facade/src/dom.dart b/modules/facade/src/dom.dart index e1d044b8fd..f01f55c656 100644 --- a/modules/facade/src/dom.dart +++ b/modules/facade/src/dom.dart @@ -68,11 +68,8 @@ class DOM { static clone(Node node) { return node.clone(true); } - static setProperty(Element element, String name, value) { - new JsObject.fromBrowserObject(element)[name] = value; - } - static getProperty(Element element, String name) { - return new JsObject.fromBrowserObject(element)[name]; + static hasProperty(Element element, String name) { + return new JsObject.fromBrowserObject(element).hasProperty(name); } static getElementsByClassName(Element element, String name) { return element.getElementsByClassName(name); diff --git a/modules/facade/src/dom.es6 b/modules/facade/src/dom.es6 index 8bb76f0765..3a95395d0c 100644 --- a/modules/facade/src/dom.es6 +++ b/modules/facade/src/dom.es6 @@ -63,11 +63,8 @@ export class DOM { static clone(node:Node) { return node.cloneNode(true); } - static setProperty(element:Element, name:string, value) { - element[name] = value; - } - static getProperty(element:Element, name:string) { - return element[name]; + static hasProperty(element:Element, name:string) { + return name in element; } static getElementsByClassName(element:Element, name:string) { return element.getElementsByClassName(name);