diff --git a/modules/angular2/angular2.ts b/modules/angular2/angular2.ts index 1c80720275..e1b961307e 100644 --- a/modules/angular2/angular2.ts +++ b/modules/angular2/angular2.ts @@ -58,11 +58,13 @@ export { export * from './http'; export { - EventDispatcher, + RenderEventDispatcher, Renderer, RenderElementRef, RenderViewRef, - RenderProtoViewRef + RenderProtoViewRef, + RenderFragmentRef, + RenderViewWithFragments } from 'angular2/src/render/api'; export { DomRenderer, diff --git a/modules/angular2/docs/core/02_directives.md b/modules/angular2/docs/core/02_directives.md index 6e991d6380..f72df4e61d 100644 --- a/modules/angular2/docs/core/02_directives.md +++ b/modules/angular2/docs/core/02_directives.md @@ -137,7 +137,7 @@ class Pane { | Component controller class

{{title}}

- +
``` diff --git a/modules/angular2/src/core/compiler/compiler.ts b/modules/angular2/src/core/compiler/compiler.ts index 1f11489cb8..eeccfa6900 100644 --- a/modules/angular2/src/core/compiler/compiler.ts +++ b/modules/angular2/src/core/compiler/compiler.ts @@ -15,8 +15,7 @@ import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection import {DirectiveResolver} from './directive_resolver'; -import {AppProtoView} from './view'; -import {ElementBinder} from './element_binder'; +import {AppProtoView, AppProtoViewMergeMapping} from './view'; import {ProtoViewRef} from './view_ref'; import {DirectiveBinding} from './element_injector'; import {ViewResolver} from './view_resolver'; @@ -91,6 +90,7 @@ export class Compiler { private _appUrl: string; private _render: renderApi.RenderCompiler; private _protoViewFactory: ProtoViewFactory; + private _unmergedCyclicEmbeddedProtoViews: RecursiveEmbeddedProtoView[] = []; /** * @private @@ -137,13 +137,17 @@ export class Compiler { Compiler._assertTypeIsComponent(componentBinding); var directiveMetadata = componentBinding.metadata; - hostPvPromise = this._render.compileHost(directiveMetadata) - .then((hostRenderPv) => { - return this._compileNestedProtoViews(componentBinding, hostRenderPv, - [componentBinding]); - }); + hostPvPromise = + this._render.compileHost(directiveMetadata) + .then((hostRenderPv) => { + var protoView = this._protoViewFactory.createAppProtoViews( + componentBinding, hostRenderPv, [componentBinding]); + this._compilerCache.setHost(componentType, protoView); + return this._compileNestedProtoViews(hostRenderPv, protoView, componentType); + }); } - return hostPvPromise.then((hostAppProtoView) => { return new ProtoViewRef(hostAppProtoView); }); + return hostPvPromise.then(hostAppProtoView => this._mergeCyclicEmbeddedProtoViews().then( + _ => new ProtoViewRef(hostAppProtoView))); } private _compile(componentBinding: DirectiveBinding): Promise| AppProtoView { @@ -156,12 +160,12 @@ export class Compiler { return protoView; } - var pvPromise = this._compiling.get(component); - if (isPresent(pvPromise)) { + var resultPromise = this._compiling.get(component); + if (isPresent(resultPromise)) { // The component is already being compiled, attach to the existing Promise // instead of re-compiling the component. // It happens when a template references a component multiple times. - return pvPromise; + return resultPromise; } var view = this._viewResolver.resolve(component); @@ -178,14 +182,19 @@ export class Compiler { ListWrapper.map(directives, (directive) => this._bindDirective(directive))); var renderTemplate = this._buildRenderTemplate(component, view, boundDirectives); - pvPromise = - this._render.compile(renderTemplate) - .then((renderPv) => { - return this._compileNestedProtoViews(componentBinding, renderPv, boundDirectives); - }); + resultPromise = this._render.compile(renderTemplate) + .then((renderPv) => { + var protoView = this._protoViewFactory.createAppProtoViews( + componentBinding, renderPv, boundDirectives); + // Populate the cache before compiling the nested components, + // so that components can reference themselves in their template. + this._compilerCache.set(component, protoView); + MapWrapper.delete(this._compiling, component); - this._compiling.set(component, pvPromise); - return pvPromise; + return this._compileNestedProtoViews(renderPv, protoView, component); + }); + this._compiling.set(component, resultPromise); + return resultPromise; } private _removeDuplicatedDirectives(directives: List): List { @@ -194,24 +203,11 @@ export class Compiler { return MapWrapper.values(directivesMap); } - private _compileNestedProtoViews(componentBinding, renderPv, directives): Promise| - AppProtoView { - var protoViews = - this._protoViewFactory.createAppProtoViews(componentBinding, renderPv, directives); - var protoView = protoViews[0]; - if (isPresent(componentBinding)) { - var component = componentBinding.key.token; - if (renderPv.type === renderApi.ViewType.COMPONENT) { - // Populate the cache before compiling the nested components, - // so that components can reference themselves in their template. - this._compilerCache.set(component, protoView); - MapWrapper.delete(this._compiling, component); - } else { - this._compilerCache.setHost(component, protoView); - } - } + private _compileNestedProtoViews(renderProtoView: renderApi.ProtoViewDto, + appProtoView: AppProtoView, + componentType: Type): Promise { var nestedPVPromises = []; - ListWrapper.forEach(this._collectComponentElementBinders(protoViews), (elementBinder) => { + this._loopComponentElementBinders(appProtoView, (parentPv, elementBinder) => { var nestedComponent = elementBinder.componentDirective; var elementBinderDone = (nestedPv: AppProtoView) => { elementBinder.nestedProtoView = nestedPv; }; @@ -222,24 +218,85 @@ export class Compiler { elementBinderDone(nestedCall); } }); - - if (nestedPVPromises.length > 0) { - return PromiseWrapper.all(nestedPVPromises).then((_) => protoView); - } else { - return protoView; - } + return PromiseWrapper.all(nestedPVPromises) + .then((_) => { + var appProtoViewsToMergeInto = []; + var mergeRenderProtoViews = this._collectMergeRenderProtoViewsRecurse( + renderProtoView, appProtoView, appProtoViewsToMergeInto); + if (isBlank(mergeRenderProtoViews)) { + throw new BaseException(`Unconditional component cycle in ${stringify(componentType)}`); + } + return this._mergeProtoViews(appProtoViewsToMergeInto, mergeRenderProtoViews); + }); } - private _collectComponentElementBinders(protoViews: List): List { - var componentElementBinders = []; - ListWrapper.forEach(protoViews, (protoView) => { - ListWrapper.forEach(protoView.elementBinders, (elementBinder) => { - if (isPresent(elementBinder.componentDirective)) { - componentElementBinders.push(elementBinder); + private _mergeProtoViews( + appProtoViewsToMergeInto: AppProtoView[], + mergeRenderProtoViews: + List>): Promise { + return this._render.mergeProtoViewsRecursively(mergeRenderProtoViews) + .then((mergeResults: List) => { + // Note: We don't need to check for nulls here as we filtered them out before! + // (in RenderCompiler.mergeProtoViewsRecursively and + // _collectMergeRenderProtoViewsRecurse). + for (var i = 0; i < mergeResults.length; i++) { + appProtoViewsToMergeInto[i].mergeMapping = + new AppProtoViewMergeMapping(mergeResults[i]); + } + return appProtoViewsToMergeInto[0]; + }); + } + + private _loopComponentElementBinders(appProtoView: AppProtoView, callback: Function) { + appProtoView.elementBinders.forEach((elementBinder) => { + if (isPresent(elementBinder.componentDirective)) { + callback(appProtoView, elementBinder); + } else if (isPresent(elementBinder.nestedProtoView)) { + this._loopComponentElementBinders(elementBinder.nestedProtoView, callback); + } + }); + } + + private _collectMergeRenderProtoViewsRecurse( + renderProtoView: renderApi.ProtoViewDto, appProtoView: AppProtoView, + targetAppProtoViews: AppProtoView[]): List> { + targetAppProtoViews.push(appProtoView); + var result = [renderProtoView.render]; + for (var i = 0; i < appProtoView.elementBinders.length; i++) { + var binder = appProtoView.elementBinders[i]; + if (binder.hasStaticComponent()) { + if (isBlank(binder.nestedProtoView.mergeMapping)) { + // cycle via an embedded ProtoView. store the AppProtoView and ProtoViewDto for later. + this._unmergedCyclicEmbeddedProtoViews.push( + new RecursiveEmbeddedProtoView(appProtoView, renderProtoView)); + return null; + } + result.push(binder.nestedProtoView.mergeMapping.renderProtoViewRef); + } else if (binder.hasEmbeddedProtoView()) { + result.push(this._collectMergeRenderProtoViewsRecurse( + renderProtoView.elementBinders[i].nestedProtoView, binder.nestedProtoView, + targetAppProtoViews)); + } + } + return result; + } + + private _mergeCyclicEmbeddedProtoViews() { + var pvs = this._unmergedCyclicEmbeddedProtoViews; + this._unmergedCyclicEmbeddedProtoViews = []; + var promises = pvs.map(entry => { + var appProtoView = entry.appProtoView; + var mergeRenderProtoViews = [entry.renderProtoView.render]; + appProtoView.elementBinders.forEach((binder) => { + if (binder.hasStaticComponent()) { + mergeRenderProtoViews.push(binder.nestedProtoView.mergeMapping.renderProtoViewRef); + } else if (binder.hasEmbeddedProtoView()) { + mergeRenderProtoViews.push(null); } }); + return this._mergeProtoViews([appProtoView], mergeRenderProtoViews); }); - return componentElementBinders; + return PromiseWrapper.all(promises); } private _buildRenderTemplate(component, view, directives): renderApi.ViewDefinition { @@ -299,3 +356,7 @@ export class Compiler { } } } + +class RecursiveEmbeddedProtoView { + constructor(public appProtoView: AppProtoView, public renderProtoView: renderApi.ProtoViewDto) {} +} diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 7036ec9a37..5c41fc0267 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -278,7 +278,7 @@ export class DirectiveBinding extends ResolvedBinding { // TODO(rado): benchmark and consider rolling in as ElementInjector fields. export class PreBuiltObjects { constructor(public viewManager: avmModule.AppViewManager, public view: viewModule.AppView, - public protoView: viewModule.AppProtoView) {} + public elementRef: ElementRef, public protoView: viewModule.AppProtoView) {} } export class EventEmitterAccessor { @@ -575,7 +575,7 @@ export class ElementInjector extends TreeNode implements Depend getComponent(): any { return this._strategy.getComponent(); } - getElementRef(): ElementRef { return this._preBuiltObjects.view.elementRefs[this._proto.index]; } + getElementRef(): ElementRef { return this._preBuiltObjects.elementRef; } getViewContainerRef(): ViewContainerRef { return new ViewContainerRef(this._preBuiltObjects.viewManager, this.getElementRef()); @@ -606,7 +606,8 @@ export class ElementInjector extends TreeNode implements Depend // We provide the component's view change detector to components and // the surrounding component's change detector to directives. if (dirBin.metadata.type === DirectiveMetadata.COMPONENT_TYPE) { - var componentView = this._preBuiltObjects.view.componentChildViews[this._proto.index]; + var componentView = this._preBuiltObjects.view.getNestedView( + this._preBuiltObjects.elementRef.boundElementIndex); return componentView.changeDetector.ref; } else { return this._preBuiltObjects.view.changeDetector.ref; diff --git a/modules/angular2/src/core/compiler/element_ref.ts b/modules/angular2/src/core/compiler/element_ref.ts index 7585e6fd87..9fcea3e815 100644 --- a/modules/angular2/src/core/compiler/element_ref.ts +++ b/modules/angular2/src/core/compiler/element_ref.ts @@ -1,4 +1,4 @@ -import {BaseException} from 'angular2/src/facade/lang'; +import {BaseException, isPresent} from 'angular2/src/facade/lang'; import {ViewRef} from './view_ref'; import {RenderViewRef, RenderElementRef, Renderer} from 'angular2/src/render/api'; @@ -24,9 +24,18 @@ export class ElementRef implements RenderElementRef { */ boundElementIndex: number; - constructor(parentView: ViewRef, boundElementIndex: number, private _renderer: Renderer) { + /** + * Index of the element inside the {@link RenderViewRef}. + * + * This is used internally by the Angular framework to locate elements. + */ + renderBoundElementIndex: number; + + constructor(parentView: ViewRef, boundElementIndex: number, renderBoundElementIndex: number, + private _renderer: Renderer) { this.parentView = parentView; this.boundElementIndex = boundElementIndex; + this.renderBoundElementIndex = renderBoundElementIndex; } /** diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index a6fa85e10f..974295a3dc 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -11,7 +11,8 @@ import { DirectiveRecord, ProtoChangeDetector, DEFAULT, - ChangeDetectorDefinition + ChangeDetectorDefinition, + ASTWithSource } from 'angular2/change_detection'; import * as renderApi from 'angular2/src/render/api'; @@ -21,16 +22,16 @@ import {ProtoElementInjector, DirectiveBinding} from './element_injector'; class BindingRecordsCreator { _directiveRecordsMap: Map = new Map(); - _textNodeIndex: number = 0; - getBindingRecords(elementBinders: List, + getBindingRecords(textBindings: List, + elementBinders: List, allDirectiveMetadatas: List): List { var bindings = []; + this._createTextNodeRecords(bindings, textBindings); for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; boundElementIndex++) { var renderElementBinder = elementBinders[boundElementIndex]; - this._createTextNodeRecords(bindings, renderElementBinder); this._createElementPropertyRecords(bindings, boundElementIndex, renderElementBinder); this._createDirectiveRecords(bindings, boundElementIndex, renderElementBinder.directives, allDirectiveMetadatas); @@ -55,13 +56,10 @@ class BindingRecordsCreator { return directiveRecords; } - _createTextNodeRecords(bindings: List, - renderElementBinder: renderApi.ElementBinder) { - if (isBlank(renderElementBinder.textBindings)) return; - - ListWrapper.forEach(renderElementBinder.textBindings, (b) => { - bindings.push(BindingRecord.createForTextNode(b, this._textNodeIndex++)); - }); + _createTextNodeRecords(bindings: List, textBindings: List) { + for (var i = 0; i < textBindings.length; i++) { + bindings.push(BindingRecord.createForTextNode(textBindings[i], i)); + } } _createElementPropertyRecords(bindings: List, boundElementIndex: number, @@ -162,7 +160,7 @@ export class ProtoViewFactory { createAppProtoViews(hostComponentBinding: DirectiveBinding, rootRenderProtoView: renderApi.ProtoViewDto, - allDirectives: List): List { + allDirectives: List): AppProtoView { var allRenderDirectiveMetadata = ListWrapper.map(allDirectives, directiveBinding => directiveBinding.metadata); var nestedPvsWithIndex = _collectNestedProtoViews(rootRenderProtoView); @@ -176,7 +174,7 @@ export class ProtoViewFactory { changeDetectorDefs, changeDetectorDef => this._changeDetection.createProtoChangeDetector(changeDetectorDef)); var appProtoViews = ListWrapper.createFixedSize(nestedPvsWithIndex.length); - ListWrapper.forEach(nestedPvsWithIndex, (pvWithIndex) => { + ListWrapper.forEach(nestedPvsWithIndex, (pvWithIndex: RenderProtoViewWithIndex) => { var appProtoView = _createAppProtoView(pvWithIndex.renderProtoView, protoChangeDetectors[pvWithIndex.index], nestedPvVariableBindings[pvWithIndex.index], allDirectives); @@ -186,7 +184,7 @@ export class ProtoViewFactory { } appProtoViews[pvWithIndex.index] = appProtoView; }); - return appProtoViews; + return appProtoViews[0]; } } @@ -209,6 +207,7 @@ function _collectNestedProtoViews( if (isBlank(result)) { result = []; } + // reserve the place in the array result.push( new RenderProtoViewWithIndex(renderProtoView, result.length, parentIndex, boundElementIndex)); var currentIndex = result.length - 1; @@ -230,8 +229,8 @@ function _getChangeDetectorDefinitions( return ListWrapper.map(nestedPvsWithIndex, (pvWithIndex) => { var elementBinders = pvWithIndex.renderProtoView.elementBinders; var bindingRecordsCreator = new BindingRecordsCreator(); - var bindingRecords = - bindingRecordsCreator.getBindingRecords(elementBinders, allRenderDirectiveMetadata); + var bindingRecords = bindingRecordsCreator.getBindingRecords( + pvWithIndex.renderProtoView.textBindings, elementBinders, allRenderDirectiveMetadata); var directiveRecords = bindingRecordsCreator.getDirectiveRecords(elementBinders, allRenderDirectiveMetadata); var strategyName = DEFAULT; @@ -255,8 +254,9 @@ function _createAppProtoView( renderProtoView: renderApi.ProtoViewDto, protoChangeDetector: ProtoChangeDetector, variableBindings: Map, allDirectives: List): AppProtoView { var elementBinders = renderProtoView.elementBinders; - var protoView = new AppProtoView(renderProtoView.render, protoChangeDetector, variableBindings, - createVariableLocations(elementBinders)); + var protoView = new AppProtoView(renderProtoView.type, protoChangeDetector, variableBindings, + createVariableLocations(elementBinders), + renderProtoView.textBindings.length); _createElementBinders(protoView, elementBinders, allDirectives); _bindDirectiveEvents(protoView, elementBinders); diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index 11dbd54f2a..6390b55634 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -20,10 +20,46 @@ import { import {ElementBinder} from './element_binder'; import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; import * as renderApi from 'angular2/src/render/api'; -import {EventDispatcher} from 'angular2/src/render/api'; -import {ViewRef} from './view_ref'; +import {RenderEventDispatcher} from 'angular2/src/render/api'; +import {ViewRef, internalView} from './view_ref'; import {ElementRef} from './element_ref'; +export class AppProtoViewMergeMapping { + renderProtoViewRef: renderApi.RenderProtoViewRef; + renderFragmentCount: number; + renderElementIndices: number[]; + renderInverseElementIndices: number[]; + renderTextIndices: number[]; + nestedViewIndicesByElementIndex: number[]; + hostElementIndicesByViewIndex: number[]; + constructor(renderProtoViewMergeMapping: renderApi.RenderProtoViewMergeMapping) { + this.renderProtoViewRef = renderProtoViewMergeMapping.mergedProtoViewRef; + this.renderFragmentCount = renderProtoViewMergeMapping.fragmentCount; + this.renderElementIndices = renderProtoViewMergeMapping.mappedElementIndices; + this.renderInverseElementIndices = + inverseIndexMapping(this.renderElementIndices, this.renderElementIndices.length); + this.renderTextIndices = renderProtoViewMergeMapping.mappedTextIndices; + this.hostElementIndicesByViewIndex = renderProtoViewMergeMapping.hostElementIndicesByViewIndex; + this.nestedViewIndicesByElementIndex = + inverseIndexMapping(this.hostElementIndicesByViewIndex, this.renderElementIndices.length); + } + + get viewCount() { return this.hostElementIndicesByViewIndex.length; } + + get elementCount() { return this.renderElementIndices.length; } +} + +function inverseIndexMapping(input: number[], resultLength: number): number[] { + var result = ListWrapper.createFixedSize(resultLength); + for (var i = 0; i < input.length; i++) { + var value = input[i]; + if (isPresent(value)) { + result[input[i]] = i; + } + } + return result; +} + export class AppViewContainer { // The order in this list matches the DOM order. views: List = []; @@ -33,17 +69,34 @@ export class AppViewContainer { * Cost of making objects: http://jsperf.com/instantiate-size-of-object * */ -export class AppView implements ChangeDispatcher, EventDispatcher { - render: renderApi.RenderViewRef = null; - /// This list matches the _nodes list. It is sparse, since only Elements have ElementInjector +export class AppView implements ChangeDispatcher, RenderEventDispatcher { + // AppViews that have been merged in depth first order. + // This list is shared between all merged views. Use this.elementOffset to get the local + // entries. + views: List = null; + // root elementInjectors of this AppView + // This list is local to this AppView and not shared with other Views. rootElementInjectors: List; + // ElementInjectors of all AppViews in views grouped by view. + // This list is shared between all merged views. Use this.elementOffset to get the local + // entries. elementInjectors: List = null; - changeDetector: ChangeDetector = null; - componentChildViews: List = null; - viewContainers: List; + // ViewContainers of all AppViews in views grouped by view. + // This list is shared between all merged views. Use this.elementOffset to get the local + // entries. + viewContainers: List = null; + // PreBuiltObjects of all AppViews in views grouped by view. + // This list is shared between all merged views. Use this.elementOffset to get the local + // entries. preBuiltObjects: List = null; + // ElementRef of all AppViews in views grouped by view. + // This list is shared between all merged views. Use this.elementOffset to get the local + // entries. elementRefs: List; + ref: ViewRef; + changeDetector: ChangeDetector = null; + /** * The context against which data-binding expressions in this view are evaluated against. @@ -60,24 +113,26 @@ export class AppView implements ChangeDispatcher, EventDispatcher { locals: Locals; constructor(public renderer: renderApi.Renderer, public proto: AppProtoView, - protoLocals: Map) { - this.viewContainers = ListWrapper.createFixedSize(this.proto.elementBinders.length); - this.elementRefs = ListWrapper.createFixedSize(this.proto.elementBinders.length); + public mainMergeMapping: AppProtoViewMergeMapping, public viewOffset: number, + public elementOffset: number, public textOffset: number, + protoLocals: Map, public render: renderApi.RenderViewRef, + public renderFragment: renderApi.RenderFragmentRef) { this.ref = new ViewRef(this); - for (var i = 0; i < this.elementRefs.length; i++) { - this.elementRefs[i] = new ElementRef(this.ref, i, renderer); - } + this.locals = new Locals(null, MapWrapper.clone(protoLocals)); // TODO optimize this } init(changeDetector: ChangeDetector, elementInjectors: List, rootElementInjectors: List, preBuiltObjects: List, - componentChildViews: List) { + views: List, elementRefs: List, + viewContainers: List) { this.changeDetector = changeDetector; this.elementInjectors = elementInjectors; this.rootElementInjectors = rootElementInjectors; this.preBuiltObjects = preBuiltObjects; - this.componentChildViews = componentChildViews; + this.views = views; + this.elementRefs = elementRefs; + this.viewContainers = viewContainers; } setLocal(contextName: string, value): void { @@ -98,49 +153,57 @@ export class AppView implements ChangeDispatcher, EventDispatcher { * * @param {string} eventName * @param {*} eventObj - * @param {int} binderIndex + * @param {int} boundElementIndex */ - triggerEventHandlers(eventName: string, eventObj, binderIndex: int): void { + triggerEventHandlers(eventName: string, eventObj, boundElementIndex: int): void { var locals = new Map(); locals.set('$event', eventObj); - this.dispatchEvent(binderIndex, eventName, locals); + this.dispatchEvent(boundElementIndex, eventName, locals); } // dispatch to element injector or text nodes based on context notifyOnBinding(b: BindingRecord, currentValue: any): void { - if (b.isElementProperty()) { - this.renderer.setElementProperty(this.elementRefs[b.elementIndex], b.propertyName, - currentValue); - } else if (b.isElementAttribute()) { - this.renderer.setElementAttribute(this.elementRefs[b.elementIndex], b.propertyName, - currentValue); - } else if (b.isElementClass()) { - this.renderer.setElementClass(this.elementRefs[b.elementIndex], b.propertyName, currentValue); - } else if (b.isElementStyle()) { - var unit = isPresent(b.propertyUnit) ? b.propertyUnit : ''; - this.renderer.setElementStyle(this.elementRefs[b.elementIndex], b.propertyName, - `${currentValue}${unit}`); - } else if (b.isTextNode()) { - this.renderer.setText(this.render, b.elementIndex, currentValue); + if (b.isTextNode()) { + this.renderer.setText( + this.render, this.mainMergeMapping.renderTextIndices[b.elementIndex + this.textOffset], + currentValue); } else { - throw new BaseException('Unsupported directive record'); + var elementRef = this.elementRefs[this.elementOffset + b.elementIndex]; + if (b.isElementProperty()) { + this.renderer.setElementProperty(elementRef, b.propertyName, currentValue); + } else if (b.isElementAttribute()) { + this.renderer.setElementAttribute(elementRef, b.propertyName, currentValue); + } else if (b.isElementClass()) { + this.renderer.setElementClass(elementRef, b.propertyName, currentValue); + } else if (b.isElementStyle()) { + var unit = isPresent(b.propertyUnit) ? b.propertyUnit : ''; + this.renderer.setElementStyle(elementRef, b.propertyName, `${currentValue}${unit}`); + } else { + throw new BaseException('Unsupported directive record'); + } } } notifyOnAllChangesDone(): void { + var eiCount = this.proto.elementBinders.length; var ei = this.elementInjectors; - for (var i = ei.length - 1; i >= 0; i--) { - if (isPresent(ei[i])) ei[i].onAllChangesDone(); + for (var i = eiCount - 1; i >= 0; i--) { + if (isPresent(ei[i + this.elementOffset])) ei[i + this.elementOffset].onAllChangesDone(); } } getDirectiveFor(directive: DirectiveIndex): any { - var elementInjector = this.elementInjectors[directive.elementIndex]; + var elementInjector = this.elementInjectors[this.elementOffset + directive.elementIndex]; return elementInjector.getDirectiveAtIndex(directive.directiveIndex); } + getNestedView(boundElementIndex: number): AppView { + var viewIndex = this.mainMergeMapping.nestedViewIndicesByElementIndex[boundElementIndex]; + return isPresent(viewIndex) ? this.views[viewIndex] : null; + } + getDetectorFor(directive: DirectiveIndex): any { - var childView = this.componentChildViews[directive.elementIndex]; + var childView = this.getNestedView(this.elementOffset + directive.elementIndex); return isPresent(childView) ? childView.changeDetector : null; } @@ -148,15 +211,24 @@ export class AppView implements ChangeDispatcher, EventDispatcher { this.renderer.invokeElementMethod(this.elementRefs[elementIndex], methodName, args); } - // implementation of EventDispatcher#dispatchEvent + // implementation of RenderEventDispatcher#dispatchRenderEvent + dispatchRenderEvent(renderElementIndex: number, eventName: string, + locals: Map): boolean { + var elementRef = + this.elementRefs[this.proto.mergeMapping.renderInverseElementIndices[renderElementIndex]]; + var view = internalView(elementRef.parentView); + return view.dispatchEvent(elementRef.boundElementIndex, eventName, locals); + } + + // returns false if preventDefault must be applied to the DOM event - dispatchEvent(elementIndex: number, eventName: string, locals: Map): boolean { + dispatchEvent(boundElementIndex: number, eventName: string, locals: Map): boolean { // Most of the time the event will be fired only when the view is in the live document. // However, in a rare circumstance the view might get dehydrated, in between the event // queuing up and firing. var allowDefaultBehavior = true; if (this.hydrated()) { - var elBinder = this.proto.elementBinders[elementIndex]; + var elBinder = this.proto.elementBinders[boundElementIndex - this.elementOffset]; if (isBlank(elBinder.hostListeners)) return allowDefaultBehavior; var eventMap = elBinder.hostListeners[eventName]; if (isBlank(eventMap)) return allowDefaultBehavior; @@ -165,7 +237,7 @@ export class AppView implements ChangeDispatcher, EventDispatcher { if (directiveIndex === -1) { context = this.context; } else { - context = this.elementInjectors[elementIndex].getDirectiveAtIndex(directiveIndex); + context = this.elementInjectors[boundElementIndex].getDirectiveAtIndex(directiveIndex); } var result = expr.eval(context, new Locals(this.locals, locals)); if (isPresent(result)) { @@ -183,11 +255,11 @@ export class AppView implements ChangeDispatcher, EventDispatcher { export class AppProtoView { elementBinders: List = []; protoLocals: Map = new Map(); + mergeMapping: AppProtoViewMergeMapping; - constructor(public render: renderApi.RenderProtoViewRef, - public protoChangeDetector: ProtoChangeDetector, + constructor(public type: renderApi.ViewType, public protoChangeDetector: ProtoChangeDetector, public variableBindings: Map, - public variableLocations: Map) { + public variableLocations: Map, public textBindingCount: number) { if (isPresent(variableBindings)) { MapWrapper.forEach(variableBindings, (templateName, _) => { this.protoLocals.set(templateName, null); }); diff --git a/modules/angular2/src/core/compiler/view_manager.ts b/modules/angular2/src/core/compiler/view_manager.ts index 0f412aa2e8..94cbab48c4 100644 --- a/modules/angular2/src/core/compiler/view_manager.ts +++ b/modules/angular2/src/core/compiler/view_manager.ts @@ -4,7 +4,13 @@ import * as viewModule from './view'; import {ElementRef} from './element_ref'; import {ProtoViewRef, ViewRef, internalView, internalProtoView} from './view_ref'; import {ViewContainerRef} from './view_container_ref'; -import {Renderer, RenderViewRef} from 'angular2/src/render/api'; +import { + Renderer, + RenderViewRef, + RenderFragmentRef, + RenderViewWithFragments, + ViewType +} from 'angular2/src/render/api'; import {AppViewManagerUtils} from './view_manager_utils'; import {AppViewPool} from './view_pool'; import {AppViewListener} from './view_listener'; @@ -22,18 +28,6 @@ export class AppViewManager { constructor(private _viewPool: AppViewPool, private _viewListener: AppViewListener, private _utils: AppViewManagerUtils, private _renderer: Renderer) {} - /** - * Returns associated Component {@link ViewRef} from {@link ElementRef}. - * - * If an {@link ElementRef} is from an element which has a component, this method returns - * the component's {@link ViewRef}. - */ - getComponentView(hostLocation: ElementRef): ViewRef { - var hostView: viewModule.AppView = internalView(hostLocation.parentView); - var boundElementIndex = hostLocation.boundElementIndex; - return hostView.componentChildViews[boundElementIndex].ref; - } - /** * Returns a {@link ViewContainerRef} at the {@link ElementRef} location. */ @@ -47,7 +41,8 @@ export class AppViewManager { */ // TODO(misko): remove https://github.com/angular/angular/issues/2891 getHostElement(hostViewRef: ViewRef): ElementRef { - return internalView(hostViewRef).elementRefs[0]; + var hostView = internalView(hostViewRef); + return hostView.elementRefs[hostView.elementOffset]; } /** @@ -62,15 +57,15 @@ export class AppViewManager { getNamedElementInComponentView(hostLocation: ElementRef, variableName: string): ElementRef { var hostView = internalView(hostLocation.parentView); var boundElementIndex = hostLocation.boundElementIndex; - var componentView = hostView.componentChildViews[boundElementIndex]; + var componentView = hostView.getNestedView(boundElementIndex); if (isBlank(componentView)) { throw new BaseException(`There is no component directive at element ${boundElementIndex}`); } - var elementIndex = componentView.proto.variableLocations.get(variableName); - if (isBlank(elementIndex)) { + var binderIdx = componentView.proto.variableLocations.get(variableName); + if (isBlank(binderIdx)) { throw new BaseException(`Could not find variable ${variableName}`); } - return componentView.elementRefs[elementIndex]; + return componentView.elementRefs[componentView.elementOffset + binderIdx]; } /** @@ -146,14 +141,13 @@ export class AppViewManager { if (isBlank(hostElementSelector)) { hostElementSelector = hostProtoView.elementBinders[0].componentDirective.metadata.selector; } - var renderView = this._renderer.createRootHostView(hostProtoView.render, hostElementSelector); - var hostView = this._utils.createView(hostProtoView, renderView, this, this._renderer); - this._renderer.setEventDispatcher(hostView.render, hostView); - this._createViewRecurse(hostView); - this._viewListener.viewCreated(hostView); + var renderViewWithFragments = this._renderer.createRootHostView( + hostProtoView.mergeMapping.renderProtoViewRef, + hostProtoView.mergeMapping.renderFragmentCount, hostElementSelector); + var hostView = this._createMainView(hostProtoView, renderViewWithFragments); + this._renderer.hydrateView(hostView.render); this._utils.hydrateRootHostView(hostView, injector); - this._viewHydrateRecurse(hostView); return hostView.ref; } @@ -162,14 +156,14 @@ export class AppViewManager { * Remove the View created with {@link AppViewManager#createRootHostView}. */ destroyRootHostView(hostViewRef: ViewRef) { - // Note: Don't detach the hostView as we want to leave the - // root element in place. Also don't put the hostView into the view pool + // Note: Don't put the hostView into the view pool // as it is depending on the element for which it was created. var hostView = internalView(hostViewRef); - // We do want to destroy the component view though. - this._viewDehydrateRecurse(hostView, true); - this._renderer.destroyView(hostView.render); + this._renderer.detachFragment(hostView.renderFragment); + this._renderer.dehydrateView(hostView.render); + this._viewDehydrateRecurse(hostView); this._viewListener.viewDestroyed(hostView); + this._renderer.destroyView(hostView.render); } /** @@ -187,19 +181,42 @@ export class AppViewManager { if (isPresent(context)) { contextView = internalView(context.parentView); contextBoundElementIndex = context.boundElementIndex; + } else { + contextView = parentView; + contextBoundElementIndex = boundElementIndex; } - var view = this._createPooledView(protoView); - - this._renderer.attachViewInContainer(viewContainerLocation, atIndex, view.render); + var embeddedFragmentView = contextView.getNestedView(contextBoundElementIndex); + var view; + if (isPresent(embeddedFragmentView) && !embeddedFragmentView.hydrated()) { + // Case 1: instantiate the first view of a template that has been merged into a parent + view = embeddedFragmentView; + this._attachRenderView(parentView, boundElementIndex, atIndex, view); + } else { + // Case 2: instantiate another copy of the template. This is a separate case + // as we only inline one copy of the template into the parent view. + view = this._createPooledView(protoView); + this._attachRenderView(parentView, boundElementIndex, atIndex, view); + this._renderer.hydrateView(view.render); + } this._utils.attachViewInContainer(parentView, boundElementIndex, contextView, contextBoundElementIndex, atIndex, view); this._utils.hydrateViewInContainer(parentView, boundElementIndex, contextView, contextBoundElementIndex, atIndex, bindings); - this._viewHydrateRecurse(view); return view.ref; } + _attachRenderView(parentView: viewModule.AppView, boundElementIndex: number, atIndex: number, + view: viewModule.AppView) { + var elementRef = parentView.elementRefs[boundElementIndex]; + if (atIndex === 0) { + this._renderer.attachFragmentAfterElement(elementRef, view.renderFragment); + } else { + var prevView = parentView.viewContainers[boundElementIndex].views[atIndex - 1]; + this._renderer.attachFragmentAfterFragment(prevView.renderFragment, view.renderFragment); + } + } + /** * * See {@link AppViewManager#createViewInContainer}. @@ -226,7 +243,7 @@ export class AppViewManager { // Right now we are destroying any special // context view that might have been used. this._utils.attachViewInContainer(parentView, boundElementIndex, null, null, atIndex, view); - this._renderer.attachViewInContainer(viewContainerLocation, atIndex, view.render); + this._attachRenderView(parentView, boundElementIndex, atIndex, view); return viewRef; } @@ -240,86 +257,65 @@ export class AppViewManager { var viewContainer = parentView.viewContainers[boundElementIndex]; var view = viewContainer.views[atIndex]; this._utils.detachViewInContainer(parentView, boundElementIndex, atIndex); - this._renderer.detachViewInContainer(viewContainerLocation, atIndex, view.render); + this._renderer.detachFragment(view.renderFragment); return view.ref; } + _createMainView(protoView: viewModule.AppProtoView, + renderViewWithFragments: RenderViewWithFragments): viewModule.AppView { + var mergedParentView = + this._utils.createView(protoView, renderViewWithFragments, this, this._renderer); + this._renderer.setEventDispatcher(mergedParentView.render, mergedParentView); + this._viewListener.viewCreated(mergedParentView); + return mergedParentView; + } + _createPooledView(protoView: viewModule.AppProtoView): viewModule.AppView { var view = this._viewPool.getView(protoView); if (isBlank(view)) { - view = this._utils.createView(protoView, this._renderer.createView(protoView.render), this, - this._renderer); - this._renderer.setEventDispatcher(view.render, view); - this._createViewRecurse(view); - this._viewListener.viewCreated(view); + view = this._createMainView( + protoView, this._renderer.createView(protoView.mergeMapping.renderProtoViewRef, + protoView.mergeMapping.renderFragmentCount)); } return view; } - _createViewRecurse(view: viewModule.AppView) { - var binders = view.proto.elementBinders; - for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { - var binder = binders[binderIdx]; - if (binder.hasStaticComponent()) { - var childView = this._createPooledView(binder.nestedProtoView); - this._renderer.attachComponentView(view.elementRefs[binderIdx], childView.render); - this._utils.attachComponentView(view, binderIdx, childView); - } - } - } - _destroyPooledView(view: viewModule.AppView) { var wasReturned = this._viewPool.returnView(view); if (!wasReturned) { - this._renderer.destroyView(view.render); this._viewListener.viewDestroyed(view); + this._renderer.destroyView(view.render); } } - _destroyViewInContainer(parentView, boundElementIndex, atIndex: number) { + _destroyViewInContainer(parentView: viewModule.AppView, boundElementIndex: number, + atIndex: number) { var viewContainer = parentView.viewContainers[boundElementIndex]; var view = viewContainer.views[atIndex]; - this._viewDehydrateRecurse(view, false); + + this._viewDehydrateRecurse(view); this._utils.detachViewInContainer(parentView, boundElementIndex, atIndex); - this._renderer.detachViewInContainer(parentView.elementRefs[boundElementIndex], atIndex, - view.render); - this._destroyPooledView(view); - } - - _destroyComponentView(hostView, boundElementIndex, componentView) { - this._viewDehydrateRecurse(componentView, false); - this._renderer.detachComponentView(hostView.elementRefs[boundElementIndex], - componentView.render); - this._utils.detachComponentView(hostView, boundElementIndex); - this._destroyPooledView(componentView); - } - - _viewHydrateRecurse(view: viewModule.AppView) { - this._renderer.hydrateView(view.render); - - var binders = view.proto.elementBinders; - for (var i = 0; i < binders.length; ++i) { - if (binders[i].hasStaticComponent()) { - this._utils.hydrateComponentView(view, i); - this._viewHydrateRecurse(view.componentChildViews[i]); - } + if (view.viewOffset > 0) { + // Case 1: a view that is part of another view. + // Just detach the fragment + this._renderer.detachFragment(view.renderFragment); + } else { + // Case 2: a view that is not part of another view. + // dehydrate and destroy it. + this._renderer.dehydrateView(view.render); + this._renderer.detachFragment(view.renderFragment); + this._destroyPooledView(view); } } - _viewDehydrateRecurse(view: viewModule.AppView, forceDestroyComponents) { - this._utils.dehydrateView(view); - this._renderer.dehydrateView(view.render); - var binders = view.proto.elementBinders; - for (var i = 0; i < binders.length; i++) { - var componentView = view.componentChildViews[i]; - if (isPresent(componentView)) { - if (forceDestroyComponents) { - this._destroyComponentView(view, i, componentView); - } else { - this._viewDehydrateRecurse(componentView, false); - } - } - var vc = view.viewContainers[i]; + _viewDehydrateRecurse(view: viewModule.AppView) { + if (view.hydrated()) { + this._utils.dehydrateView(view); + } + var viewContainers = view.viewContainers; + for (var i = view.elementOffset, ii = view.elementOffset + view.proto.mergeMapping.elementCount; + i < ii; i++) { + var vc = viewContainers[i]; if (isPresent(vc)) { for (var j = vc.views.length - 1; j >= 0; j--) { this._destroyViewInContainer(view, i, j); diff --git a/modules/angular2/src/core/compiler/view_manager_utils.ts b/modules/angular2/src/core/compiler/view_manager_utils.ts index 49ee1874a8..eb9e2c0877 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.ts +++ b/modules/angular2/src/core/compiler/view_manager_utils.ts @@ -3,10 +3,12 @@ import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src import * as eli from './element_injector'; import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; import * as viewModule from './view'; +import {internalView} from './view_ref'; import * as avmModule from './view_manager'; -import {Renderer} from 'angular2/src/render/api'; +import {ElementRef} from './element_ref'; +import {Renderer, RenderViewWithFragments} from 'angular2/src/render/api'; import {Locals} from 'angular2/change_detection'; -import {RenderViewRef} from 'angular2/src/render/api'; +import {RenderViewRef, RenderFragmentRef, ViewType} from 'angular2/src/render/api'; @Injectable() export class AppViewManagerUtils { @@ -17,68 +19,84 @@ export class AppViewManagerUtils { return eli.getComponent(); } - createView(protoView: viewModule.AppProtoView, renderView: RenderViewRef, + createView(mergedParentViewProto: viewModule.AppProtoView, + renderViewWithFragments: RenderViewWithFragments, viewManager: avmModule.AppViewManager, renderer: Renderer): viewModule.AppView { - var view = new viewModule.AppView(renderer, protoView, protoView.protoLocals); - // TODO(tbosch): pass RenderViewRef as argument to AppView! - view.render = renderView; + var renderFragments = renderViewWithFragments.fragmentRefs; + var renderView = renderViewWithFragments.viewRef; - var changeDetector = protoView.protoChangeDetector.instantiate(view); + var elementCount = mergedParentViewProto.mergeMapping.elementCount; + var viewCount = mergedParentViewProto.mergeMapping.viewCount; + var elementRefs: ElementRef[] = ListWrapper.createFixedSize(elementCount); + var viewContainers = ListWrapper.createFixedSize(elementCount); + var preBuiltObjects: eli.PreBuiltObjects[] = ListWrapper.createFixedSize(elementCount); + var elementInjectors = ListWrapper.createFixedSize(elementCount); + var views = ListWrapper.createFixedSize(viewCount); - var binders = protoView.elementBinders; - var elementInjectors = ListWrapper.createFixedSize(binders.length); - var rootElementInjectors = []; - var preBuiltObjects = ListWrapper.createFixedSize(binders.length); - var componentChildViews = ListWrapper.createFixedSize(binders.length); + var elementOffset = 0; + var textOffset = 0; + var fragmentIdx = 0; + for (var viewOffset = 0; viewOffset < viewCount; viewOffset++) { + var hostElementIndex = + mergedParentViewProto.mergeMapping.hostElementIndicesByViewIndex[viewOffset]; + var parentView = isPresent(hostElementIndex) ? + internalView(elementRefs[hostElementIndex].parentView) : + null; + var protoView = + isPresent(hostElementIndex) ? + parentView.proto.elementBinders[hostElementIndex - parentView.elementOffset] + .nestedProtoView : + mergedParentViewProto; + var renderFragment = null; + if (viewOffset === 0 || protoView.type === ViewType.EMBEDDED) { + renderFragment = renderFragments[fragmentIdx++]; + } + var currentView = new viewModule.AppView( + renderer, protoView, mergedParentViewProto.mergeMapping, viewOffset, elementOffset, + textOffset, protoView.protoLocals, renderView, renderFragment); + views[viewOffset] = currentView; + var rootElementInjectors = []; + for (var binderIdx = 0; binderIdx < protoView.elementBinders.length; binderIdx++) { + var binder = protoView.elementBinders[binderIdx]; + var boundElementIndex = elementOffset + binderIdx; + var elementInjector = null; - for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { - var binder = binders[binderIdx]; - var elementInjector = null; + // elementInjectors and rootElementInjectors + var protoElementInjector = binder.protoElementInjector; + if (isPresent(protoElementInjector)) { + if (isPresent(protoElementInjector.parent)) { + var parentElementInjector = + elementInjectors[elementOffset + protoElementInjector.parent.index]; + elementInjector = protoElementInjector.instantiate(parentElementInjector); + } else { + elementInjector = protoElementInjector.instantiate(null); + rootElementInjectors.push(elementInjector); + } + } + elementInjectors[boundElementIndex] = elementInjector; - // elementInjectors and rootElementInjectors - var protoElementInjector = binder.protoElementInjector; - if (isPresent(protoElementInjector)) { - if (isPresent(protoElementInjector.parent)) { - var parentElementInjector = elementInjectors[protoElementInjector.parent.index]; - elementInjector = protoElementInjector.instantiate(parentElementInjector); - } else { - elementInjector = protoElementInjector.instantiate(null); - rootElementInjectors.push(elementInjector); + // elementRefs + var el = new ElementRef( + currentView.ref, boundElementIndex, + mergedParentViewProto.mergeMapping.renderElementIndices[boundElementIndex], renderer); + elementRefs[el.boundElementIndex] = el; + + // preBuiltObjects + if (isPresent(elementInjector)) { + var embeddedProtoView = binder.hasEmbeddedProtoView() ? binder.nestedProtoView : null; + preBuiltObjects[boundElementIndex] = + new eli.PreBuiltObjects(viewManager, currentView, el, embeddedProtoView); } } - elementInjectors[binderIdx] = elementInjector; - - // preBuiltObjects - if (isPresent(elementInjector)) { - var embeddedProtoView = binder.hasEmbeddedProtoView() ? binder.nestedProtoView : null; - preBuiltObjects[binderIdx] = new eli.PreBuiltObjects(viewManager, view, embeddedProtoView); + currentView.init(protoView.protoChangeDetector.instantiate(currentView), elementInjectors, + rootElementInjectors, preBuiltObjects, views, elementRefs, viewContainers); + if (isPresent(parentView) && protoView.type === ViewType.COMPONENT) { + parentView.changeDetector.addShadowDomChild(currentView.changeDetector); } + elementOffset += protoView.elementBinders.length; + textOffset += protoView.textBindingCount; } - - view.init(changeDetector, elementInjectors, rootElementInjectors, preBuiltObjects, - componentChildViews); - - return view; - } - - attachComponentView(hostView: viewModule.AppView, boundElementIndex: number, - componentView: viewModule.AppView) { - var childChangeDetector = componentView.changeDetector; - hostView.changeDetector.addShadowDomChild(childChangeDetector); - hostView.componentChildViews[boundElementIndex] = componentView; - } - - detachComponentView(hostView: viewModule.AppView, boundElementIndex: number) { - var componentView = hostView.componentChildViews[boundElementIndex]; - hostView.changeDetector.removeShadowDomChild(componentView.changeDetector); - hostView.componentChildViews[boundElementIndex] = null; - } - - hydrateComponentView(hostView: viewModule.AppView, boundElementIndex: number) { - var elementInjector = hostView.elementInjectors[boundElementIndex]; - var componentView = hostView.componentChildViews[boundElementIndex]; - var component = this.getComponentInstance(hostView, boundElementIndex); - this._hydrateView(componentView, null, elementInjector, component, null); + return views[0]; } hydrateRootHostView(hostView: viewModule.AppView, injector: Injector) { @@ -94,7 +112,11 @@ export class AppViewManagerUtils { contextBoundElementIndex = boundElementIndex; } parentView.changeDetector.addChild(view.changeDetector); - var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex); + var viewContainer = parentView.viewContainers[boundElementIndex]; + if (isBlank(viewContainer)) { + viewContainer = new viewModule.AppViewContainer(); + parentView.viewContainers[boundElementIndex] = viewContainer; + } ListWrapper.insert(viewContainer.views, atIndex, view); var sibling; if (atIndex == 0) { @@ -124,7 +146,9 @@ export class AppViewManagerUtils { inj.unlink(); } else { var removeIdx = ListWrapper.indexOf(parentView.rootElementInjectors, inj); - ListWrapper.removeAt(parentView.rootElementInjectors, removeIdx); + if (removeIdx >= 0) { + ListWrapper.removeAt(parentView.rootElementInjectors, removeIdx); + } } } } @@ -145,25 +169,45 @@ export class AppViewManagerUtils { contextView.locals); } - _hydrateView(view: viewModule.AppView, imperativelyCreatedInjector: Injector, + _hydrateView(initView: viewModule.AppView, imperativelyCreatedInjector: Injector, hostElementInjector: eli.ElementInjector, context: Object, parentLocals: Locals) { - view.context = context; - view.locals.parent = parentLocals; + var viewIdx = initView.viewOffset; + var endViewOffset = viewIdx + initView.proto.mergeMapping.viewCount; + while (viewIdx < endViewOffset) { + var currView = initView.views[viewIdx]; + var currProtoView = currView.proto; + if (currView !== initView && currView.proto.type === ViewType.EMBEDDED) { + // Don't hydrate components of embedded fragment views. + viewIdx += currProtoView.mergeMapping.viewCount; + } else { + if (currView !== initView) { + // hydrate a nested component view + imperativelyCreatedInjector = null; + parentLocals = null; + var hostElementIndex = initView.mainMergeMapping.hostElementIndicesByViewIndex[viewIdx]; + hostElementInjector = initView.elementInjectors[hostElementIndex]; + context = hostElementInjector.getComponent(); + } + currView.context = context; + currView.locals.parent = parentLocals; + var binders = currProtoView.elementBinders; + for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { + var boundElementIndex = binderIdx + currView.elementOffset; + var elementInjector = initView.elementInjectors[boundElementIndex]; - var binders = view.proto.elementBinders; - for (var i = 0; i < binders.length; ++i) { - var elementInjector = view.elementInjectors[i]; - - if (isPresent(elementInjector)) { - elementInjector.hydrate(imperativelyCreatedInjector, hostElementInjector, - view.preBuiltObjects[i]); - this._populateViewLocals(view, elementInjector); - this._setUpEventEmitters(view, elementInjector, i); - this._setUpHostActions(view, elementInjector, i); + if (isPresent(elementInjector)) { + elementInjector.hydrate(imperativelyCreatedInjector, hostElementInjector, + currView.preBuiltObjects[boundElementIndex]); + this._populateViewLocals(currView, elementInjector, boundElementIndex); + this._setUpEventEmitters(currView, elementInjector, boundElementIndex); + this._setUpHostActions(currView, elementInjector, boundElementIndex); + } + } + var pipes = this._getPipes(imperativelyCreatedInjector, hostElementInjector); + currView.changeDetector.hydrate(currView.context, currView.locals, currView, pipes); + viewIdx++; } } - var pipes = this._getPipes(imperativelyCreatedInjector, hostElementInjector); - view.changeDetector.hydrate(view.context, view.locals, view, pipes); } _getPipes(imperativelyCreatedInjector: Injector, hostElementInjector: eli.ElementInjector) { @@ -174,11 +218,12 @@ export class AppViewManagerUtils { return null; } - _populateViewLocals(view: viewModule.AppView, elementInjector: eli.ElementInjector): void { + _populateViewLocals(view: viewModule.AppView, elementInjector: eli.ElementInjector, + boundElementIdx: number): void { if (isPresent(elementInjector.getDirectiveVariableBindings())) { MapWrapper.forEach(elementInjector.getDirectiveVariableBindings(), (directiveIndex, name) => { if (isBlank(directiveIndex)) { - view.locals.set(name, elementInjector.getElementRef().nativeElement); + view.locals.set(name, view.elementRefs[boundElementIdx].nativeElement); } else { view.locals.set(name, elementInjector.getDirectiveAtIndex(directiveIndex)); } @@ -186,15 +231,6 @@ export class AppViewManagerUtils { } } - _getOrCreateViewContainer(parentView: viewModule.AppView, boundElementIndex: number) { - var viewContainer = parentView.viewContainers[boundElementIndex]; - if (isBlank(viewContainer)) { - viewContainer = new viewModule.AppViewContainer(); - parentView.viewContainers[boundElementIndex] = viewContainer; - } - return viewContainer; - } - _setUpEventEmitters(view: viewModule.AppView, elementInjector: eli.ElementInjector, boundElementIndex: number) { var emitters = elementInjector.getEventEmitterAccessors(); @@ -223,18 +259,25 @@ export class AppViewManagerUtils { } } - dehydrateView(view: viewModule.AppView) { - var binders = view.proto.elementBinders; - for (var i = 0; i < binders.length; ++i) { - var elementInjector = view.elementInjectors[i]; - if (isPresent(elementInjector)) { - elementInjector.dehydrate(); + dehydrateView(initView: viewModule.AppView) { + for (var viewIdx = initView.viewOffset, + endViewOffset = viewIdx + initView.proto.mergeMapping.viewCount; + viewIdx < endViewOffset; viewIdx++) { + var currView = initView.views[viewIdx]; + if (currView.hydrated()) { + if (isPresent(currView.locals)) { + currView.locals.clearValues(); + } + currView.context = null; + currView.changeDetector.dehydrate(); + var binders = currView.proto.elementBinders; + for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { + var eli = initView.elementInjectors[currView.elementOffset + binderIdx]; + if (isPresent(eli)) { + eli.dehydrate(); + } + } } } - if (isPresent(view.locals)) { - view.locals.clearValues(); - } - view.context = null; - view.changeDetector.dehydrate(); } } diff --git a/modules/angular2/src/core/compiler/view_ref.ts b/modules/angular2/src/core/compiler/view_ref.ts index 3a5747bb61..343859ac11 100644 --- a/modules/angular2/src/core/compiler/view_ref.ts +++ b/modules/angular2/src/core/compiler/view_ref.ts @@ -1,6 +1,6 @@ import {isPresent} from 'angular2/src/facade/lang'; import * as viewModule from './view'; -import {RenderViewRef} from 'angular2/src/render/api'; +import {RenderViewRef, RenderFragmentRef} from 'angular2/src/render/api'; // This is a workaround for privacy in Dart as we don't have library parts export function internalView(viewRef: ViewRef): viewModule.AppView { @@ -71,6 +71,11 @@ export class ViewRef { */ get render(): RenderViewRef { return this._view.render; } + /** + * Return {@link RenderFragmentRef} + */ + get renderFragment(): RenderFragmentRef { return this._view.renderFragment; } + /** * Set local variable for a view. * diff --git a/modules/angular2/src/debug/debug_element.ts b/modules/angular2/src/debug/debug_element.ts index 64f7b8597c..e8edbdee93 100644 --- a/modules/angular2/src/debug/debug_element.ts +++ b/modules/angular2/src/debug/debug_element.ts @@ -45,9 +45,7 @@ export class DebugElement { * @return {List} */ get children(): List { - var thisElementBinder = this._parentView.proto.elementBinders[this._boundElementIndex]; - - return this._getChildElements(this._parentView, thisElementBinder.index); + return this._getChildElements(this._parentView, this._boundElementIndex); } /** @@ -57,7 +55,7 @@ export class DebugElement { * @return {List} */ get componentViewChildren(): List { - var shadowView = this._parentView.componentChildViews[this._boundElementIndex]; + var shadowView = this._parentView.getNestedView(this._boundElementIndex); if (!isPresent(shadowView)) { // The current element is not a component. @@ -120,14 +118,14 @@ export class DebugElement { var els = []; var parentElementBinder = null; if (isPresent(parentBoundElementIndex)) { - parentElementBinder = view.proto.elementBinders[parentBoundElementIndex]; + parentElementBinder = view.proto.elementBinders[parentBoundElementIndex - view.elementOffset]; } for (var i = 0; i < view.proto.elementBinders.length; ++i) { var binder = view.proto.elementBinders[i]; if (binder.parent == parentElementBinder) { - els.push(new DebugElement(view, i)); + els.push(new DebugElement(view, view.elementOffset + i)); - var views = view.viewContainers[i]; + var views = view.viewContainers[view.elementOffset + i]; if (isPresent(views)) { ListWrapper.forEach(views.views, (nextView) => { els = ListWrapper.concat(els, this._getChildElements(nextView, null)); @@ -184,7 +182,11 @@ export class By { static all(): Function { return (debugElement) => true; } static css(selector: string): Predicate { - return (debugElement) => { return DOM.elementMatches(debugElement.nativeElement, selector); }; + return (debugElement) => { + return isPresent(debugElement.nativeElement) ? + DOM.elementMatches(debugElement.nativeElement, selector) : + false; + }; } static directive(type: Type): Predicate { return (debugElement) => { return debugElement.hasDirective(type); }; diff --git a/modules/angular2/src/debug/debug_element_view_listener.ts b/modules/angular2/src/debug/debug_element_view_listener.ts index b759aa7e17..abe6e88d4e 100644 --- a/modules/angular2/src/debug/debug_element_view_listener.ts +++ b/modules/angular2/src/debug/debug_element_view_listener.ts @@ -56,7 +56,8 @@ export class DebugElementViewListener implements AppViewListener { _allViewsById.set(viewId, view); _allIdsByView.set(view, viewId); for (var i = 0; i < view.elementRefs.length; i++) { - _setElementId(this._renderer.getNativeElementSync(view.elementRefs[i]), [viewId, i]); + var el = view.elementRefs[i]; + _setElementId(this._renderer.getNativeElementSync(el), [viewId, i]); } } diff --git a/modules/angular2/src/dom/browser_adapter.ts b/modules/angular2/src/dom/browser_adapter.ts index e7ee492ffb..a5554ac286 100644 --- a/modules/angular2/src/dom/browser_adapter.ts +++ b/modules/angular2/src/dom/browser_adapter.ts @@ -63,7 +63,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { get attrToPropMap(): any { return _attrToPropMap; } query(selector: string): any { return document.querySelector(selector); } - querySelector(el, selector: string): Node { return el.querySelector(selector); } + querySelector(el, selector: string): HTMLElement { return el.querySelector(selector); } querySelectorAll(el, selector: string): List { return el.querySelectorAll(selector); } on(el, evt, listener) { el.addEventListener(evt, listener, false); } onAndCancel(el, evt, listener): Function { @@ -112,17 +112,16 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { return res; } clearNodes(el) { - for (var i = 0; i < el.childNodes.length; i++) { - this.remove(el.childNodes[i]); + while (el.firstChild) { + el.firstChild.remove(); } } appendChild(el, node) { el.appendChild(node); } removeChild(el, node) { el.removeChild(node); } replaceChild(el: Node, newChild, oldChild) { el.replaceChild(newChild, oldChild); } - remove(el): Node { - var parent = el.parentNode; - parent.removeChild(el); - return el; + remove(node): Node { + node.remove(); + return node; } insertBefore(el, node) { el.parentNode.insertBefore(node, el); } insertAllBefore(el, nodes) { diff --git a/modules/angular2/src/dom/dom_adapter.ts b/modules/angular2/src/dom/dom_adapter.ts index 79f1e26c58..ea15ab8053 100644 --- a/modules/angular2/src/dom/dom_adapter.ts +++ b/modules/angular2/src/dom/dom_adapter.ts @@ -31,7 +31,7 @@ export class DomAdapter { parse(templateHtml: string) { throw _abstract(); } query(selector: string): any { throw _abstract(); } - querySelector(el, selector: string) { throw _abstract(); } + querySelector(el, selector: string): HTMLElement { throw _abstract(); } querySelectorAll(el, selector: string): List { throw _abstract(); } on(el, evt, listener) { throw _abstract(); } onAndCancel(el, evt, listener): Function { throw _abstract(); } @@ -76,7 +76,7 @@ export class DomAdapter { getShadowRoot(el): any { throw _abstract(); } getHost(el): any { throw _abstract(); } getDistributedNodes(el): List { throw _abstract(); } - clone(node: Node): Node { throw _abstract(); } + clone /**/ (node: Node /*T*/): Node /*T*/ { throw _abstract(); } getElementsByClassName(element, name: string): List { throw _abstract(); } getElementsByTagName(element, name: string): List { throw _abstract(); } classList(element): List { throw _abstract(); } @@ -105,7 +105,7 @@ export class DomAdapter { isElementNode(node): boolean { throw _abstract(); } hasShadowRoot(node): boolean { throw _abstract(); } isShadowRoot(node): boolean { throw _abstract(); } - importIntoDoc(node) { throw _abstract(); } + importIntoDoc /**/ (node: Node /*T*/): Node /*T*/ { throw _abstract(); } isPageRule(rule): boolean { throw _abstract(); } isStyleRule(rule): boolean { throw _abstract(); } isMediaRule(rule): boolean { throw _abstract(); } diff --git a/modules/angular2/src/dom/parse5_adapter.ts b/modules/angular2/src/dom/parse5_adapter.ts index 7fdc66578a..54c0127d8b 100644 --- a/modules/angular2/src/dom/parse5_adapter.ts +++ b/modules/angular2/src/dom/parse5_adapter.ts @@ -19,6 +19,8 @@ var _attrToPropMap = { }; var defDoc = null; +var mapProps = ['attribs', 'x-attribsNamespace', 'x-attribsPrefix']; + function _notImplemented(methodName) { return new BaseException('This method is not implemented in Parse5DomAdapter: ' + methodName); } @@ -271,18 +273,45 @@ export class Parse5DomAdapter extends DomAdapter { getHost(el): string { return el.host; } getDistributedNodes(el: any): List { throw _notImplemented('getDistributedNodes'); } clone(node: Node): Node { - // e.g. document fragment - if ((node).type === 'root') { - var serialized = serializer.serialize(node); - var newParser = new parse5.Parser(parse5.TreeAdapters.htmlparser2); - return newParser.parseFragment(serialized); - } else { - var temp = treeAdapter.createElement("template", null, []); - treeAdapter.appendChild(temp, node); - var serialized = serializer.serialize(temp); - var newParser = new parse5.Parser(parse5.TreeAdapters.htmlparser2); - return newParser.parseFragment(serialized).childNodes[0]; - } + var _recursive = (node) => { + var nodeClone = Object.create(Object.getPrototypeOf(node)); + for (var prop in node) { + var desc = Object.getOwnPropertyDescriptor(node, prop); + if (desc && 'value' in desc && typeof desc.value !== 'object') { + nodeClone[prop] = node[prop]; + } + } + nodeClone.parent = null; + nodeClone.prev = null; + nodeClone.next = null; + nodeClone.children = null; + + mapProps.forEach(mapName => { + if (isPresent(node[mapName])) { + nodeClone[mapName] = {}; + for (var prop in node[mapName]) { + nodeClone[mapName][prop] = node[mapName][prop]; + } + } + }); + var cNodes = node.children; + if (cNodes) { + var cNodesClone = new Array(cNodes.length); + for (var i = 0; i < cNodes.length; i++) { + var childNode = cNodes[i]; + var childNodeClone = _recursive(childNode); + cNodesClone[i] = childNodeClone; + if (i > 0) { + childNodeClone.prev = cNodesClone[i - 1]; + cNodesClone[i - 1].next = childNodeClone; + } + childNodeClone.parent = nodeClone; + } + nodeClone.children = cNodesClone; + } + return nodeClone; + }; + return _recursive(node); } getElementsByClassName(element, name: string): List { return this.querySelectorAll(element, "." + name); diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index 73aae5ccec..95d0496eb0 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -221,13 +221,17 @@ bool isJsObject(o) { return false; } +var _assertionsEnabled = null; bool assertionsEnabled() { - try { - assert(false); - return false; - } catch (e) { - return true; + if (_assertionsEnabled == null) { + try { + assert(false); + _assertionsEnabled = false; + } catch (e) { + _assertionsEnabled = true; + } } + return _assertionsEnabled; } // Can't be all uppercase as our transpiler would think it is a special directive... diff --git a/modules/angular2/src/render/api.ts b/modules/angular2/src/render/api.ts index 5df4301972..1ca61a26d7 100644 --- a/modules/angular2/src/render/api.ts +++ b/modules/angular2/src/render/api.ts @@ -48,11 +48,10 @@ export class ElementBinder { // that replaced the values that should be extracted from the element // with a local name eventBindings: List; - textBindings: List; readAttributes: Map; constructor({index, parentIndex, distanceToParent, directives, nestedProtoView, propertyBindings, - variableBindings, eventBindings, textBindings, readAttributes}: { + variableBindings, eventBindings, readAttributes}: { index?: number, parentIndex?: number, distanceToParent?: number, @@ -61,7 +60,6 @@ export class ElementBinder { propertyBindings?: List, variableBindings?: Map, eventBindings?: List, - textBindings?: List, readAttributes?: Map } = {}) { this.index = index; @@ -72,7 +70,6 @@ export class ElementBinder { this.propertyBindings = propertyBindings; this.variableBindings = variableBindings; this.eventBindings = eventBindings; - this.textBindings = textBindings; this.readAttributes = readAttributes; } } @@ -116,17 +113,20 @@ export class ProtoViewDto { elementBinders: List; variableBindings: Map; type: ViewType; + textBindings: List; - constructor({render, elementBinders, variableBindings, type}: { + constructor({render, elementBinders, variableBindings, type, textBindings}: { render?: RenderProtoViewRef, elementBinders?: List, variableBindings?: Map, - type?: ViewType + type?: ViewType, + textBindings?: List }) { this.render = render; this.elementBinders = elementBinders; this.variableBindings = variableBindings; this.type = type; + this.textBindings = textBindings; } } @@ -260,10 +260,13 @@ export class DirectiveMetadata { } } -// An opaque reference to a DomProtoView +// An opaque reference to a render proto ivew export class RenderProtoViewRef {} -// An opaque reference to a DomView +// An opaque reference to a part of a view +export class RenderFragmentRef {} + +// An opaque reference to a view export class RenderViewRef {} export class ViewDefinition { @@ -291,6 +294,23 @@ export class ViewDefinition { } } +export class RenderProtoViewMergeMapping { + constructor(public mergedProtoViewRef: RenderProtoViewRef, + // Number of fragments in the merged ProtoView. + // Fragments are stored in depth first order of nested ProtoViews. + public fragmentCount: number, + // Mapping from app element index to render element index. + // Mappings of nested ProtoViews are in depth first order, with all + // indices for one ProtoView in a consecuitve block. + public mappedElementIndices: number[], + // Mapping from app text index to render text index. + // Mappings of nested ProtoViews are in depth first order, with all + // indices for one ProtoView in a consecuitve block. + public mappedTextIndices: number[], + // Mapping from view index to app element index + public hostElementIndicesByViewIndex: number[]) {} +} + export class RenderCompiler { /** * Creats a ProtoViewDto that contains a single nested component with the given componentId. @@ -303,6 +323,24 @@ export class RenderCompiler { * but only the needed ones based on previous calls. */ compile(view: ViewDefinition): Promise { return null; } + + /** + * Merges ProtoViews. + * The first entry of the array is the protoview into which all the other entries of the array + * should be merged. + * If the array contains other arrays, they will be merged before processing the parent array. + * The array must contain an entry for every component and embedded ProtoView of the first entry. + * @param protoViewRefs List of ProtoViewRefs or nested + * @return the merge result for every input array in depth first order. + */ + mergeProtoViewsRecursively( + protoViewRefs: List>): Promise { + return null; + } +} + +export class RenderViewWithFragments { + constructor(public viewRef: RenderViewRef, public fragmentRefs: RenderFragmentRef[]) {} } /** @@ -315,33 +353,39 @@ export interface RenderElementRef { * Reference to the {@link RenderViewRef} where the `RenderElementRef` is inside of. */ renderView: RenderViewRef; - /** - * Index of the element inside the {@link ViewRef}. + * Index of the element inside the {@link RenderViewRef}. * * This is used internally by the Angular framework to locate elements. */ - boundElementIndex: number; + renderBoundElementIndex: number; } export class Renderer { /** * Creates a root host view that includes the given element. + * Note that the fragmentCount needs to be passed in so that we can create a result + * synchronously even when dealing with webworkers! + * * @param {RenderProtoViewRef} hostProtoViewRef a RenderProtoViewRef of type * ProtoViewDto.HOST_VIEW_TYPE * @param {any} hostElementSelector css selector for the host element (will be queried against the * main document) - * @return {RenderViewRef} the created view + * @return {RenderViewWithFragments} the created view including fragments */ - createRootHostView(hostProtoViewRef: RenderProtoViewRef, - hostElementSelector: string): RenderViewRef { + createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number, + hostElementSelector: string): RenderViewWithFragments { return null; } /** - * Creates a regular view out of the given ProtoView + * Creates a regular view out of the given ProtoView. + * Note that the fragmentCount needs to be passed in so that we can create a result + * synchronously even when dealing with webworkers! */ - createView(protoViewRef: RenderProtoViewRef): RenderViewRef { return null; } + createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments { + return null; + } /** * Destroys the given view after it has been dehydrated and detached @@ -349,27 +393,20 @@ export class Renderer { destroyView(viewRef: RenderViewRef) {} /** - * Attaches a componentView into the given hostView at the given element + * Attaches a fragment after another fragment. */ - attachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) {} + attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef, + fragmentRef: RenderFragmentRef) {} /** - * Detaches a componentView into the given hostView at the given element + * Attaches a fragment after an element. */ - detachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) {} + attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {} /** - * Attaches a view into a ViewContainer (in the given parentView at the given element) at the - * given index. + * Detaches a fragment. */ - attachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) {} - - /** - * Detaches a view into a ViewContainer (in the given parentView at the given element) at the - * given index. - */ - // TODO(tbosch): this should return a promise as it can be animated! - detachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) {} + detachFragment(fragmentRef: RenderFragmentRef) {} /** * Hydrates a view after it has been attached. Hydration/dehydration is used for reusing views @@ -422,18 +459,18 @@ export class Renderer { /** * Sets the dispatcher for all events of the given view */ - setEventDispatcher(viewRef: RenderViewRef, dispatcher: EventDispatcher) {} + setEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher) {} } /** * A dispatcher for all events happening in a view. */ -export interface EventDispatcher { +export interface RenderEventDispatcher { /** * Called when an event was triggered for a on-* attribute on an element. * @param {Map} locals Locals to be used to evaluate the * event expressions */ - dispatchEvent(elementIndex: number, eventName: string, locals: Map); + dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map); } diff --git a/modules/angular2/src/render/dom/compiler/compile_pipeline.ts b/modules/angular2/src/render/dom/compiler/compile_pipeline.ts index a39a370a88..ec7d441dab 100644 --- a/modules/angular2/src/render/dom/compiler/compile_pipeline.ts +++ b/modules/angular2/src/render/dom/compiler/compile_pipeline.ts @@ -13,7 +13,9 @@ import {ProtoViewDto, ViewType} from '../../api'; */ export class CompilePipeline { _control: CompileControl; - constructor(steps: List) { this._control = new CompileControl(steps); } + constructor(steps: List, private _useNativeShadowDom: boolean = false) { + this._control = new CompileControl(steps); + } process(rootElement, protoViewType: ViewType = null, compilationCtxtDescription: string = ''): List { @@ -22,7 +24,8 @@ export class CompilePipeline { } var results = []; var rootCompileElement = new CompileElement(rootElement, compilationCtxtDescription); - rootCompileElement.inheritedProtoView = new ProtoViewBuilder(rootElement, protoViewType); + rootCompileElement.inheritedProtoView = + new ProtoViewBuilder(rootElement, protoViewType, this._useNativeShadowDom); rootCompileElement.isViewRoot = true; this._process(results, null, rootCompileElement, compilationCtxtDescription); return results; diff --git a/modules/angular2/src/render/dom/compiler/compiler.ts b/modules/angular2/src/render/dom/compiler/compiler.ts index c5a69e8025..2ba228e29e 100644 --- a/modules/angular2/src/render/dom/compiler/compiler.ts +++ b/modules/angular2/src/render/dom/compiler/compiler.ts @@ -10,13 +10,15 @@ import { ViewType, DirectiveMetadata, RenderCompiler, - RenderProtoViewRef + RenderProtoViewRef, + RenderProtoViewMergeMapping } from '../../api'; import {CompilePipeline} from './compile_pipeline'; import {ViewLoader} from 'angular2/src/render/dom/compiler/view_loader'; import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory'; import {Parser} from 'angular2/change_detection'; import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy'; +import * as pvm from '../view/proto_view_merger'; /** * The compiler loads and translates the html templates of components into @@ -24,7 +26,10 @@ import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy'; * the CompilePipeline and the CompileSteps. */ export class DomCompiler extends RenderCompiler { - constructor(public _stepFactory: CompileStepFactory, public _viewLoader: ViewLoader) { super(); } + constructor(public _stepFactory: CompileStepFactory, public _viewLoader: ViewLoader, + public _useNativeShadowDom: boolean) { + super(); + } compile(view: ViewDefinition): Promise { var tplPromise = this._viewLoader.load(view); @@ -42,13 +47,21 @@ export class DomCompiler extends RenderCompiler { styleAbsUrls: null, directives: [directiveMetadata] }); - var element = DOM.createElement(directiveMetadata.selector); - return this._compileTemplate(hostViewDef, element, ViewType.HOST); + var template = DOM.createTemplate(''); + DOM.appendChild(DOM.content(template), DOM.createElement(directiveMetadata.selector)); + return this._compileTemplate(hostViewDef, template, ViewType.HOST); + } + + mergeProtoViewsRecursively( + protoViewRefs: + List>): Promise> { + return PromiseWrapper.resolve(pvm.mergeProtoViewsRecursively(protoViewRefs)); } _compileTemplate(viewDef: ViewDefinition, tplElement, protoViewType: ViewType): Promise { - var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef)); + var pipeline = + new CompilePipeline(this._stepFactory.createSteps(viewDef), this._useNativeShadowDom); var compileElements = pipeline.process(tplElement, protoViewType, viewDef.componentId); return PromiseWrapper.resolve(compileElements[0].inheritedProtoView.build()); @@ -58,6 +71,7 @@ export class DomCompiler extends RenderCompiler { @Injectable() export class DefaultDomCompiler extends DomCompiler { constructor(parser: Parser, shadowDomStrategy: ShadowDomStrategy, viewLoader: ViewLoader) { - super(new DefaultStepFactory(parser, shadowDomStrategy), viewLoader); + super(new DefaultStepFactory(parser, shadowDomStrategy), viewLoader, + shadowDomStrategy.hasNativeContentElement()); } } diff --git a/modules/angular2/src/render/dom/compiler/selector.ts b/modules/angular2/src/render/dom/compiler/selector.ts index de342039b8..4e47d04843 100644 --- a/modules/angular2/src/render/dom/compiler/selector.ts +++ b/modules/angular2/src/render/dom/compiler/selector.ts @@ -27,11 +27,11 @@ var _SELECTOR_REGEXP = RegExpWrapper.create( */ export class CssSelector { element: string = null; - classNames: List = []; - attrs: List = []; - notSelectors: List = []; + classNames: string[] = []; + attrs: string[] = []; + notSelectors: CssSelector[] = []; - static parse(selector: string): List { + static parse(selector: string): CssSelector[] { var results: CssSelector[] = []; var _addResult = (res: CssSelector[], cssSel) => { if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) && @@ -135,21 +135,21 @@ export class CssSelector { * are contained in a given CssSelector. */ export class SelectorMatcher { - static createNotMatcher(notSelectors: List): SelectorMatcher { + static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher { var notMatcher = new SelectorMatcher(); notMatcher.addSelectables(notSelectors, null); return notMatcher; } - private _elementMap: Map> = new Map(); + private _elementMap: Map = new Map(); private _elementPartialMap: Map = new Map(); - private _classMap: Map> = new Map(); + private _classMap: Map = new Map(); private _classPartialMap: Map = new Map(); - private _attrValueMap: Map>> = new Map(); + private _attrValueMap: Map> = new Map(); private _attrValuePartialMap: Map> = new Map(); - private _listContexts: List = []; + private _listContexts: SelectorListContext[] = []; - addSelectables(cssSelectors: List, callbackCtxt?: any) { + addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) { var listContext = null; if (cssSelectors.length > 1) { listContext = new SelectorListContext(cssSelectors); @@ -220,7 +220,7 @@ export class SelectorMatcher { } } - private _addTerminal(map: Map>, name: string, + private _addTerminal(map: Map, name: string, selectable: SelectorContext) { var terminalList = map.get(name); if (isBlank(terminalList)) { @@ -298,7 +298,7 @@ export class SelectorMatcher { return result; } - _matchTerminal(map: Map>, name, cssSelector: CssSelector, + _matchTerminal(map: Map, name, cssSelector: CssSelector, matchedCallback: (CssSelector, any) => void): boolean { if (isBlank(map) || isBlank(name)) { return false; @@ -341,12 +341,12 @@ export class SelectorMatcher { class SelectorListContext { alreadyMatched: boolean = false; - constructor(public selectors: List) {} + constructor(public selectors: CssSelector[]) {} } // Store context to pass back selector and context when a selector is matched class SelectorContext { - notSelectors: List; + notSelectors: CssSelector[]; constructor(public selector: CssSelector, public cbContext: any, public listContext: SelectorListContext) { diff --git a/modules/angular2/src/render/dom/compiler/text_interpolation_parser.ts b/modules/angular2/src/render/dom/compiler/text_interpolation_parser.ts index 42f69e882b..480c5aa4b6 100644 --- a/modules/angular2/src/render/dom/compiler/text_interpolation_parser.ts +++ b/modules/angular2/src/render/dom/compiler/text_interpolation_parser.ts @@ -26,7 +26,11 @@ export class TextInterpolationParser implements CompileStep { var expr = this._parser.parseInterpolation(text, current.elementDescription); if (isPresent(expr)) { DOM.setText(node, ' '); - current.bindElement().bindText(node, expr); + if (current.element === current.inheritedProtoView.rootElement) { + current.inheritedProtoView.bindRootText(node, expr); + } else { + current.bindElement().bindText(node, expr); + } } } } diff --git a/modules/angular2/src/render/dom/compiler/view_splitter.ts b/modules/angular2/src/render/dom/compiler/view_splitter.ts index 9329b9bfd0..19b0620c79 100644 --- a/modules/angular2/src/render/dom/compiler/view_splitter.ts +++ b/modules/angular2/src/render/dom/compiler/view_splitter.ts @@ -65,23 +65,31 @@ export class ViewSplitter implements CompileStep { } } if (hasTemplateBinding) { - var newParent = new CompileElement(DOM.createTemplate('')); - newParent.inheritedProtoView = current.inheritedProtoView; - newParent.inheritedElementBinder = current.inheritedElementBinder; - newParent.distanceToInheritedBinder = current.distanceToInheritedBinder; + var anchor = new CompileElement(DOM.createTemplate('')); + anchor.inheritedProtoView = current.inheritedProtoView; + anchor.inheritedElementBinder = current.inheritedElementBinder; + anchor.distanceToInheritedBinder = current.distanceToInheritedBinder; // newParent doesn't appear in the original template, so we associate // the current element description to get a more meaningful message in case of error - newParent.elementDescription = current.elementDescription; + anchor.elementDescription = current.elementDescription; - current.inheritedProtoView = newParent.bindElement().bindNestedProtoView(current.element); + var viewRoot = new CompileElement(DOM.createTemplate('')); + viewRoot.inheritedProtoView = anchor.bindElement().bindNestedProtoView(viewRoot.element); + // viewRoot doesn't appear in the original template, so we associate + // the current element description to get a more meaningful message in case of error + viewRoot.elementDescription = current.elementDescription; + viewRoot.isViewRoot = true; + + current.inheritedProtoView = viewRoot.inheritedProtoView; current.inheritedElementBinder = null; current.distanceToInheritedBinder = 0; - current.isViewRoot = true; - this._parseTemplateBindings(templateBindings, newParent); - this._addParentElement(current.element, newParent.element); - control.addParent(newParent); - DOM.remove(current.element); + this._parseTemplateBindings(templateBindings, anchor); + DOM.insertBefore(current.element, anchor.element); + control.addParent(anchor); + + DOM.appendChild(DOM.content(viewRoot.element), current.element); + control.addParent(viewRoot); } } } @@ -94,11 +102,6 @@ export class ViewSplitter implements CompileStep { } } - _addParentElement(currentElement, newParentElement) { - DOM.insertBefore(currentElement, newParentElement); - DOM.appendChild(newParentElement, currentElement); - } - _parseTemplateBindings(templateBindings: string, compileElement: CompileElement) { var bindings = this._parser.parseTemplateBindings(templateBindings, compileElement.elementDescription); diff --git a/modules/angular2/src/render/dom/dom_renderer.ts b/modules/angular2/src/render/dom/dom_renderer.ts index dfa2890570..5f39da5655 100644 --- a/modules/angular2/src/render/dom/dom_renderer.ts +++ b/modules/angular2/src/render/dom/dom_renderer.ts @@ -10,17 +10,26 @@ import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src import {DOM} from 'angular2/src/dom/dom_adapter'; -import {Content} from './shadow_dom/content_tag'; -import {ShadowDomStrategy} from './shadow_dom/shadow_dom_strategy'; import {EventManager} from './events/event_manager'; import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './view/proto_view'; import {DomView, DomViewRef, resolveInternalDomView} from './view/view'; -import {DomElement} from './view/element'; -import {DomViewContainer} from './view/view_container'; -import {NG_BINDING_CLASS_SELECTOR, NG_BINDING_CLASS, camelCaseToDashCase} from './util'; +import {DomFragmentRef, resolveInternalDomFragment} from './view/fragment'; +import { + NG_BINDING_CLASS_SELECTOR, + NG_BINDING_CLASS, + cloneAndQueryProtoView, + camelCaseToDashCase +} from './util'; -import {Renderer, RenderProtoViewRef, RenderViewRef, RenderElementRef} from '../api'; +import { + Renderer, + RenderProtoViewRef, + RenderViewRef, + RenderElementRef, + RenderFragmentRef, + RenderViewWithFragments +} from '../api'; export const DOCUMENT_TOKEN = CONST_EXPR(new OpaqueToken('DocumentToken')); export const DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES = @@ -32,8 +41,7 @@ export class DomRenderer extends Renderer { _document; _reflectPropertiesAsAttributes: boolean; - constructor(public _eventManager: EventManager, public _shadowDomStrategy: ShadowDomStrategy, - @Inject(DOCUMENT_TOKEN) document, + constructor(public _eventManager: EventManager, @Inject(DOCUMENT_TOKEN) document, @Inject(DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES) reflectPropertiesAsAttributes: boolean) { super(); @@ -41,109 +49,57 @@ export class DomRenderer extends Renderer { this._document = document; } - createRootHostView(hostProtoViewRef: RenderProtoViewRef, - hostElementSelector: string): RenderViewRef { + createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number, + hostElementSelector: string): RenderViewWithFragments { var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef); var element = DOM.querySelector(this._document, hostElementSelector); if (isBlank(element)) { throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`); } - return new DomViewRef(this._createView(hostProtoView, element)); + return this._createView(hostProtoView, element); } - createView(protoViewRef: RenderProtoViewRef): RenderViewRef { + createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments { var protoView = resolveInternalDomProtoView(protoViewRef); - return new DomViewRef(this._createView(protoView, null)); + return this._createView(protoView, null); } - destroyView(view: RenderViewRef) { + destroyView(viewRef: RenderViewRef) { // noop for now } getNativeElementSync(location: RenderElementRef): any { + if (isBlank(location.renderBoundElementIndex)) { + return null; + } return resolveInternalDomView(location.renderView) - .boundElements[location.boundElementIndex] - .element; + .boundElements[location.renderBoundElementIndex]; } - attachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) { - var hostView = resolveInternalDomView(location.renderView); - var componentView = resolveInternalDomView(componentViewRef); - var element = hostView.boundElements[location.boundElementIndex].element; - var lightDom = hostView.boundElements[location.boundElementIndex].lightDom; - if (isPresent(lightDom)) { - lightDom.attachShadowDomView(componentView); + getRootNodes(fragment: RenderFragmentRef): List { + return resolveInternalDomFragment(fragment); + } + + attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef, + fragmentRef: RenderFragmentRef) { + var previousFragmentNodes = resolveInternalDomFragment(previousFragmentRef); + var sibling = previousFragmentNodes[previousFragmentNodes.length - 1]; + moveNodesAfterSibling(sibling, resolveInternalDomFragment(fragmentRef)); + } + + attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) { + if (isBlank(elementRef.renderBoundElementIndex)) { + return; } - var shadowRoot = this._shadowDomStrategy.prepareShadowRoot(element); - this._moveViewNodesIntoParent(shadowRoot, componentView); - componentView.hostLightDom = lightDom; - componentView.shadowRoot = shadowRoot; + var parentView = resolveInternalDomView(elementRef.renderView); + var element = parentView.boundElements[elementRef.renderBoundElementIndex]; + moveNodesAfterSibling(element, resolveInternalDomFragment(fragmentRef)); } - setComponentViewRootNodes(componentViewRef: RenderViewRef, rootNodes: List) { - var componentView = resolveInternalDomView(componentViewRef); - this._removeViewNodes(componentView); - componentView.rootNodes = rootNodes; - this._moveViewNodesIntoParent(componentView.shadowRoot, componentView); - } - - getRootNodes(viewRef: RenderViewRef): List { - return resolveInternalDomView(viewRef).rootNodes; - } - - detachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) { - var hostView = resolveInternalDomView(location.renderView); - var componentView = resolveInternalDomView(componentViewRef); - this._removeViewNodes(componentView); - var lightDom = hostView.boundElements[location.boundElementIndex].lightDom; - if (isPresent(lightDom)) { - lightDom.detachShadowDomView(); - } - componentView.hostLightDom = null; - componentView.shadowRoot = null; - } - - attachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) { - var parentView = resolveInternalDomView(location.renderView); - var view = resolveInternalDomView(viewRef); - var viewContainer = this._getOrCreateViewContainer(parentView, location.boundElementIndex); - ListWrapper.insert(viewContainer.views, atIndex, view); - view.hostLightDom = parentView.hostLightDom; - - var directParentLightDom = this._directParentLightDom(parentView, location.boundElementIndex); - if (isBlank(directParentLightDom)) { - var siblingToInsertAfter; - if (atIndex == 0) { - siblingToInsertAfter = parentView.boundElements[location.boundElementIndex].element; - } else { - siblingToInsertAfter = ListWrapper.last(viewContainer.views[atIndex - 1].rootNodes); - } - this._moveViewNodesAfterSibling(siblingToInsertAfter, view); - } else { - directParentLightDom.redistribute(); - } - // new content tags might have appeared, we need to redistribute. - if (isPresent(parentView.hostLightDom)) { - parentView.hostLightDom.redistribute(); - } - } - - detachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) { - var parentView = resolveInternalDomView(location.renderView); - var view = resolveInternalDomView(viewRef); - var viewContainer = parentView.boundElements[location.boundElementIndex].viewContainer; - var detachedView = viewContainer.views[atIndex]; - ListWrapper.removeAt(viewContainer.views, atIndex); - var directParentLightDom = this._directParentLightDom(parentView, location.boundElementIndex); - if (isBlank(directParentLightDom)) { - this._removeViewNodes(detachedView); - } else { - directParentLightDom.redistribute(); - } - view.hostLightDom = null; - // content tags might have disappeared we need to do redistribution. - if (isPresent(parentView.hostLightDom)) { - parentView.hostLightDom.redistribute(); + detachFragment(fragmentRef: RenderFragmentRef) { + var fragmentNodes = resolveInternalDomFragment(fragmentRef); + for (var i = 0; i < fragmentNodes.length; i++) { + DOM.remove(fragmentNodes[i]); } } @@ -152,13 +108,6 @@ export class DomRenderer extends Renderer { if (view.hydrated) throw new BaseException('The view is already hydrated.'); view.hydrated = true; - for (var i = 0; i < view.boundElements.length; ++i) { - var lightDom = view.boundElements[i].lightDom; - if (isPresent(lightDom)) { - lightDom.redistribute(); - } - } - // add global events view.eventHandlerRemovers = []; var binders = view.proto.elementBinders; @@ -173,9 +122,6 @@ export class DomRenderer extends Renderer { } } } - if (isPresent(view.hostLightDom)) { - view.hostLightDom.redistribute(); - } } dehydrateView(viewRef: RenderViewRef) { @@ -191,8 +137,11 @@ export class DomRenderer extends Renderer { } setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any): void { + if (isBlank(location.renderBoundElementIndex)) { + return; + } var view = resolveInternalDomView(location.renderView); - view.setElementProperty(location.boundElementIndex, propertyName, propertyValue); + view.setElementProperty(location.renderBoundElementIndex, propertyName, propertyValue); // Reflect the property value as an attribute value with ng-reflect- prefix. if (this._reflectPropertiesAsAttributes) { this.setElementAttribute(location, `${REFLECT_PREFIX}${camelCaseToDashCase(propertyName)}`, @@ -202,26 +151,41 @@ export class DomRenderer extends Renderer { setElementAttribute(location: RenderElementRef, attributeName: string, attributeValue: string): void { + if (isBlank(location.renderBoundElementIndex)) { + return; + } var view = resolveInternalDomView(location.renderView); - view.setElementAttribute(location.boundElementIndex, attributeName, attributeValue); + view.setElementAttribute(location.renderBoundElementIndex, attributeName, attributeValue); } setElementClass(location: RenderElementRef, className: string, isAdd: boolean): void { + if (isBlank(location.renderBoundElementIndex)) { + return; + } var view = resolveInternalDomView(location.renderView); - view.setElementClass(location.boundElementIndex, className, isAdd); + view.setElementClass(location.renderBoundElementIndex, className, isAdd); } setElementStyle(location: RenderElementRef, styleName: string, styleValue: string): void { + if (isBlank(location.renderBoundElementIndex)) { + return; + } var view = resolveInternalDomView(location.renderView); - view.setElementStyle(location.boundElementIndex, styleName, styleValue); + view.setElementStyle(location.renderBoundElementIndex, styleName, styleValue); } invokeElementMethod(location: RenderElementRef, methodName: string, args: List): void { + if (isBlank(location.renderBoundElementIndex)) { + return; + } var view = resolveInternalDomView(location.renderView); - view.invokeElementMethod(location.boundElementIndex, methodName, args); + view.invokeElementMethod(location.renderBoundElementIndex, methodName, args); } setText(viewRef: RenderViewRef, textNodeIndex: number, text: string): void { + if (isBlank(textNodeIndex)) { + return; + } var view = resolveInternalDomView(viewRef); DOM.setText(view.boundTextNodes[textNodeIndex], text); } @@ -231,99 +195,50 @@ export class DomRenderer extends Renderer { view.eventDispatcher = dispatcher; } - _createView(protoView: DomProtoView, inplaceElement): DomView { - var rootElementClone; - var elementsWithBindingsDynamic; - var viewRootNodes; + _createView(protoView: DomProtoView, inplaceElement: HTMLElement): RenderViewWithFragments { + var clonedProtoView = cloneAndQueryProtoView(protoView, true); + + var boundElements = clonedProtoView.boundElements; + + // adopt inplaceElement if (isPresent(inplaceElement)) { - rootElementClone = inplaceElement; - elementsWithBindingsDynamic = []; - viewRootNodes = [inplaceElement]; - } else if (protoView.isTemplateElement) { - rootElementClone = DOM.importIntoDoc(DOM.content(protoView.element)); - elementsWithBindingsDynamic = - DOM.querySelectorAll(rootElementClone, NG_BINDING_CLASS_SELECTOR); - viewRootNodes = ListWrapper.createFixedSize(protoView.rootNodeCount); - // Note: An explicit loop is the fastest way to convert a DOM array into a JS array! - var childNode = DOM.firstChild(rootElementClone); - for (var i = 0; i < protoView.rootNodeCount; i++, childNode = DOM.nextSibling(childNode)) { - viewRootNodes[i] = childNode; + if (protoView.fragmentsRootNodeCount[0] !== 1) { + throw new BaseException('Root proto views can only contain one element!'); } - } else { - rootElementClone = DOM.importIntoDoc(protoView.element); - elementsWithBindingsDynamic = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS); - viewRootNodes = [rootElementClone]; + DOM.clearNodes(inplaceElement); + var tempRoot = clonedProtoView.fragments[0][0]; + moveChildNodes(tempRoot, inplaceElement); + if (boundElements.length > 0 && boundElements[0] === tempRoot) { + boundElements[0] = inplaceElement; + } + clonedProtoView.fragments[0][0] = inplaceElement; } + var view = new DomView(protoView, clonedProtoView.boundTextNodes, boundElements); + var binders = protoView.elementBinders; - var boundTextNodes = ListWrapper.createFixedSize(protoView.boundTextNodeCount); - var boundElements = ListWrapper.createFixedSize(binders.length); - var boundTextNodeIdx = 0; - - for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { - var binder = binders[binderIdx]; - var element; - var childNodes; - if (binderIdx === 0 && protoView.rootBindingOffset === 1) { - // Note: if the root element was a template, - // the rootElementClone is a document fragment, - // which will be empty as soon as the view gets appended - // to a parent. So we store null in the boundElements array. - element = protoView.isTemplateElement ? null : rootElementClone; - childNodes = DOM.childNodes(rootElementClone); - } else { - element = elementsWithBindingsDynamic[binderIdx - protoView.rootBindingOffset]; - childNodes = DOM.childNodes(element); - } - - // boundTextNodes - var textNodeIndices = binder.textNodeIndices; - for (var i = 0; i < textNodeIndices.length; i++) { - boundTextNodes[boundTextNodeIdx++] = childNodes[textNodeIndices[i]]; - } - - // contentTags - var contentTag = null; - if (isPresent(binder.contentTagSelector)) { - contentTag = new Content(element, binder.contentTagSelector); - } - boundElements[binderIdx] = new DomElement(binder, element, contentTag); - } - - var view = new DomView(protoView, viewRootNodes, boundTextNodes, boundElements); - for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { var binder = binders[binderIdx]; var element = boundElements[binderIdx]; - var domEl = element.element; - // lightDoms - var lightDom = null; - // Note: for the root element we can't use the binder.elementIsEmpty - // information as we don't use the element from the ProtoView - // but an element from the document. - if (isPresent(binder.componentId) && (!binder.elementIsEmpty || isPresent(inplaceElement))) { - lightDom = this._shadowDomStrategy.constructLightDom(view, domEl); - } - element.lightDom = lightDom; - - // init contentTags - var contentTag = element.contentTag; - if (isPresent(contentTag)) { - var directParentLightDom = this._directParentLightDom(view, binderIdx); - contentTag.init(directParentLightDom); + // native shadow DOM + if (binder.hasNativeShadowRoot) { + var shadowRootWrapper = DOM.firstChild(element); + moveChildNodes(shadowRootWrapper, DOM.createShadowRoot(element)); + DOM.remove(shadowRootWrapper); } // events if (isPresent(binder.eventLocals) && isPresent(binder.localEvents)) { for (var i = 0; i < binder.localEvents.length; i++) { - this._createEventListener(view, domEl, binderIdx, binder.localEvents[i].name, + this._createEventListener(view, element, binderIdx, binder.localEvents[i].name, binder.eventLocals); } } } - return view; + return new RenderViewWithFragments( + new DomViewRef(view), clonedProtoView.fragments.map(nodes => new DomFragmentRef(nodes))); } _createEventListener(view, element, elementIndex, eventName, eventLocals) { @@ -331,45 +246,26 @@ export class DomRenderer extends Renderer { element, eventName, (event) => { view.dispatchEvent(elementIndex, eventName, event); }); } - - _moveViewNodesAfterSibling(sibling, view) { - for (var i = view.rootNodes.length - 1; i >= 0; --i) { - DOM.insertAfter(sibling, view.rootNodes[i]); - } - } - - _moveViewNodesIntoParent(parent, view) { - for (var i = 0; i < view.rootNodes.length; ++i) { - DOM.appendChild(parent, view.rootNodes[i]); - } - } - - _removeViewNodes(view) { - var len = view.rootNodes.length; - if (len == 0) return; - var parent = view.rootNodes[0].parentNode; - for (var i = len - 1; i >= 0; --i) { - DOM.removeChild(parent, view.rootNodes[i]); - } - } - - _getOrCreateViewContainer(parentView: DomView, boundElementIndex) { - var el = parentView.boundElements[boundElementIndex]; - var vc = el.viewContainer; - if (isBlank(vc)) { - vc = new DomViewContainer(); - el.viewContainer = vc; - } - return vc; - } - - _directParentLightDom(view: DomView, boundElementIndex: number) { - var directParentEl = view.getDirectParentElement(boundElementIndex); - return isPresent(directParentEl) ? directParentEl.lightDom : null; - } - _createGlobalEventListener(view, elementIndex, eventName, eventTarget, fullName): Function { return this._eventManager.addGlobalEventListener( eventTarget, eventName, (event) => { view.dispatchEvent(elementIndex, fullName, event); }); } } + +function moveNodesAfterSibling(sibling, nodes) { + if (isPresent(DOM.parentElement(sibling))) { + for (var i = 0; i < nodes.length; i++) { + DOM.insertBefore(sibling, nodes[i]); + } + DOM.insertBefore(nodes[nodes.length - 1], sibling); + } +} + +function moveChildNodes(source: Node, target: Node) { + var currChild = DOM.firstChild(source); + while (isPresent(currChild)) { + var nextChild = DOM.nextSibling(currChild); + DOM.appendChild(target, currChild); + currChild = nextChild; + } +} \ No newline at end of file diff --git a/modules/angular2/src/render/dom/shadow_dom/content_tag.ts b/modules/angular2/src/render/dom/shadow_dom/content_tag.ts deleted file mode 100644 index df2f616d54..0000000000 --- a/modules/angular2/src/render/dom/shadow_dom/content_tag.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as ldModule from './light_dom'; -import {DOM} from 'angular2/src/dom/dom_adapter'; -import {isPresent} from 'angular2/src/facade/lang'; -import {List, ListWrapper} from 'angular2/src/facade/collection'; - -class ContentStrategy { - nodes: List; - insert(nodes: List) {} -} - -/** - * An implementation of the content tag that is used by transcluding components. - * It is used when the content tag is not a direct child of another component, - * and thus does not affect redistribution. - */ -class RenderedContent extends ContentStrategy { - beginScript: any; - endScript; - - constructor(contentEl) { - super(); - this.beginScript = contentEl; - this.endScript = DOM.nextSibling(this.beginScript); - this.nodes = []; - } - - // Inserts the nodes in between the start and end scripts. - // Previous content is removed. - insert(nodes: List) { - this.nodes = nodes; - DOM.insertAllBefore(this.endScript, nodes); - this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]); - } - - _removeNodesUntil(node) { - var p = DOM.parentElement(this.beginScript); - for (var next = DOM.nextSibling(this.beginScript); next !== node; - next = DOM.nextSibling(this.beginScript)) { - DOM.removeChild(p, next); - } - } -} - -/** - * An implementation of the content tag that is used by transcluding components. - * It is used when the content tag is a direct child of another component, - * and thus does not get rendered but only affect the distribution of its parent component. - */ -class IntermediateContent extends ContentStrategy { - constructor(public destinationLightDom: ldModule.LightDom) { - super(); - this.nodes = []; - } - - insert(nodes: List) { - this.nodes = nodes; - this.destinationLightDom.redistribute(); - } -} - - -export class Content { - private _strategy: ContentStrategy = null; - - constructor(public contentStartElement, public select: string) {} - - init(destinationLightDom: ldModule.LightDom) { - this._strategy = isPresent(destinationLightDom) ? new IntermediateContent(destinationLightDom) : - new RenderedContent(this.contentStartElement); - } - - nodes(): List { return this._strategy.nodes; } - - insert(nodes: List) { this._strategy.insert(nodes); } -} diff --git a/modules/angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy.ts b/modules/angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy.ts index 40c9df5ad3..0d5d62fd46 100644 --- a/modules/angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy.ts +++ b/modules/angular2/src/render/dom/shadow_dom/emulated_unscoped_shadow_dom_strategy.ts @@ -1,8 +1,5 @@ import {DOM} from 'angular2/src/dom/dom_adapter'; -import * as viewModule from '../view/view'; - -import {LightDom} from './light_dom'; import {ShadowDomStrategy} from './shadow_dom_strategy'; import {insertSharedStyleText} from './util'; @@ -20,12 +17,6 @@ export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy { hasNativeContentElement(): boolean { return false; } - prepareShadowRoot(el): Node { return el; } - - constructLightDom(lightDomView: viewModule.DomView, el): LightDom { - return new LightDom(lightDomView, el); - } - processStyleElement(hostComponentId: string, templateUrl: string, styleEl): void { var cssText = DOM.getText(styleEl); insertSharedStyleText(cssText, this.styleHost, styleEl); diff --git a/modules/angular2/src/render/dom/shadow_dom/light_dom.ts b/modules/angular2/src/render/dom/shadow_dom/light_dom.ts deleted file mode 100644 index 8074b69a45..0000000000 --- a/modules/angular2/src/render/dom/shadow_dom/light_dom.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {DOM} from 'angular2/src/dom/dom_adapter'; -import {List, ListWrapper} from 'angular2/src/facade/collection'; -import {isBlank, isPresent} from 'angular2/src/facade/lang'; - -import * as viewModule from '../view/view'; -import * as elModule from '../view/element'; -import {Content} from './content_tag'; - -export class DestinationLightDom {} - -class _Root { - constructor(public node, public boundElement: elModule.DomElement) {} -} - -// TODO: LightDom should implement DestinationLightDom -// once interfaces are supported -export class LightDom { - // The light DOM of the element is enclosed inside the lightDomView - lightDomView: viewModule.DomView; - // The shadow DOM - shadowDomView: viewModule.DomView = null; - // The nodes of the light DOM - nodes: List; - private _roots: List<_Root> = null; - - constructor(lightDomView: viewModule.DomView, element) { - this.lightDomView = lightDomView; - this.nodes = DOM.childNodesAsList(element); - } - - attachShadowDomView(shadowDomView: viewModule.DomView) { this.shadowDomView = shadowDomView; } - - detachShadowDomView() { this.shadowDomView = null; } - - redistribute() { redistributeNodes(this.contentTags(), this.expandedDomNodes()); } - - contentTags(): List { - if (isPresent(this.shadowDomView)) { - return this._collectAllContentTags(this.shadowDomView, []); - } else { - return []; - } - } - - // Collects the Content directives from the view and all its child views - private _collectAllContentTags(view: viewModule.DomView, acc: List): List { - // Note: exiting early here is important as we call this function for every view - // that is added, so we have O(n^2) runtime. - // TODO(tbosch): fix the root problem, see - // https://github.com/angular/angular/issues/2298 - if (view.proto.transitiveContentTagCount === 0) { - return acc; - } - var els = view.boundElements; - for (var i = 0; i < els.length; i++) { - var el = els[i]; - if (isPresent(el.contentTag)) { - acc.push(el.contentTag); - } - if (isPresent(el.viewContainer)) { - ListWrapper.forEach(el.viewContainer.contentTagContainers(), - (view) => { this._collectAllContentTags(view, acc); }); - } - } - return acc; - } - - // Collects the nodes of the light DOM by merging: - // - nodes from enclosed ViewContainers, - // - nodes from enclosed content tags, - // - plain DOM nodes - expandedDomNodes(): List { - var res = []; - - var roots = this._findRoots(); - for (var i = 0; i < roots.length; ++i) { - var root = roots[i]; - if (isPresent(root.boundElement)) { - var vc = root.boundElement.viewContainer; - var content = root.boundElement.contentTag; - if (isPresent(vc)) { - res = ListWrapper.concat(res, vc.nodes()); - } else if (isPresent(content)) { - res = ListWrapper.concat(res, content.nodes()); - } else { - res.push(root.node); - } - } else { - res.push(root.node); - } - } - return res; - } - - // Returns a list of Roots for all the nodes of the light DOM. - // The Root object contains the DOM node and its corresponding boundElement - private _findRoots() { - if (isPresent(this._roots)) return this._roots; - - var boundElements = this.lightDomView.boundElements; - - this._roots = ListWrapper.map(this.nodes, (n) => { - var boundElement = null; - for (var i = 0; i < boundElements.length; i++) { - var boundEl = boundElements[i]; - if (isPresent(boundEl) && boundEl.element === n) { - boundElement = boundEl; - break; - } - } - return new _Root(n, boundElement); - }); - - return this._roots; - } -} - -// Projects the light DOM into the shadow DOM -function redistributeNodes(contents: List, nodes: List) { - for (var i = 0; i < contents.length; ++i) { - var content = contents[i]; - var select = content.select; - - // Empty selector is identical to - if (select.length === 0) { - content.insert(ListWrapper.clone(nodes)); - ListWrapper.clear(nodes); - } else { - var matchSelector = (n) => DOM.elementMatches(n, select); - var matchingNodes = ListWrapper.filter(nodes, matchSelector); - content.insert(matchingNodes); - ListWrapper.removeAll(nodes, matchingNodes); - } - } - for (var i = 0; i < nodes.length; i++) { - var node = nodes[i]; - if (isPresent(node.parentNode)) { - DOM.remove(nodes[i]); - } - } -} diff --git a/modules/angular2/src/render/dom/shadow_dom/native_shadow_dom_strategy.ts b/modules/angular2/src/render/dom/shadow_dom/native_shadow_dom_strategy.ts index 22b955e47a..1190676818 100644 --- a/modules/angular2/src/render/dom/shadow_dom/native_shadow_dom_strategy.ts +++ b/modules/angular2/src/render/dom/shadow_dom/native_shadow_dom_strategy.ts @@ -1,5 +1,4 @@ import {Injectable} from 'angular2/di'; -import {DOM} from 'angular2/src/dom/dom_adapter'; import {ShadowDomStrategy} from './shadow_dom_strategy'; /** @@ -10,5 +9,5 @@ import {ShadowDomStrategy} from './shadow_dom_strategy'; */ @Injectable() export class NativeShadowDomStrategy extends ShadowDomStrategy { - prepareShadowRoot(el): Node { return DOM.createShadowRoot(el); } + hasNativeContentElement(): boolean { return true; } } diff --git a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts index 3f439eca69..75df4795f8 100644 --- a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts +++ b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts @@ -1,5 +1,3 @@ -import {isBlank, isPresent, assertionsEnabled, isPromise} from 'angular2/src/facade/lang'; - import {DOM} from 'angular2/src/dom/dom_adapter'; import {CompileStep} from '../compiler/compile_step'; @@ -15,8 +13,6 @@ export class ShadowDomCompileStep implements CompileStep { var tagName = DOM.tagName(current.element).toUpperCase(); if (tagName == 'STYLE') { this._processStyleElement(current, control); - } else if (tagName == 'CONTENT') { - this._processContentElement(current); } else { var componentId = current.isBound() ? current.inheritedElementBinder.componentId : null; this._shadowDomStrategy.processElement(this._view.componentId, componentId, current.element); @@ -31,25 +27,4 @@ export class ShadowDomCompileStep implements CompileStep { // bindings. Skipping further compiler steps allow speeding up the compilation process. control.ignoreCurrentElement(); } - - _processContentElement(current: CompileElement) { - if (this._shadowDomStrategy.hasNativeContentElement()) { - return; - } - var attrs = current.attrs(); - var selector = attrs.get('select'); - selector = isPresent(selector) ? selector : ''; - - var contentStart = DOM.createScriptTag('type', 'ng/contentStart'); - if (assertionsEnabled()) { - DOM.setAttribute(contentStart, 'select', selector); - } - var contentEnd = DOM.createScriptTag('type', 'ng/contentEnd'); - DOM.insertBefore(current.element, contentStart); - DOM.insertBefore(current.element, contentEnd); - DOM.remove(current.element); - - current.element = contentStart; - current.bindElement().setContentTagSelector(selector); - } } diff --git a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_strategy.ts b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_strategy.ts index cdfdf91352..f69be75f8e 100644 --- a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_strategy.ts +++ b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_strategy.ts @@ -1,17 +1,9 @@ import {isBlank, isPresent} from 'angular2/src/facade/lang'; -import * as viewModule from '../view/view'; -import {LightDom} from './light_dom'; - export class ShadowDomStrategy { // Whether the strategy understands the native tag hasNativeContentElement(): boolean { return true; } - // Prepares and returns the (emulated) shadow root for the given element. - prepareShadowRoot(el): any { return null; } - - constructLightDom(lightDomView: viewModule.DomView, el): LightDom { return null; } - // An optional step that can modify the template style elements. processStyleElement(hostComponentId: string, templateUrl: string, styleElement): void {} diff --git a/modules/angular2/src/render/dom/util.ts b/modules/angular2/src/render/dom/util.ts index 709b16020f..9aa8f1627f 100644 --- a/modules/angular2/src/render/dom/util.ts +++ b/modules/angular2/src/render/dom/util.ts @@ -1,13 +1,21 @@ -import {StringWrapper, isPresent} from 'angular2/src/facade/lang'; +import {StringWrapper, isPresent, isBlank} from 'angular2/src/facade/lang'; +import {DOM} from 'angular2/src/dom/dom_adapter'; +import {ListWrapper} from 'angular2/src/facade/collection'; +import {DomProtoView} from './view/proto_view'; +import {DomElementBinder} from './view/element_binder'; export const NG_BINDING_CLASS_SELECTOR = '.ng-binding'; export const NG_BINDING_CLASS = 'ng-binding'; export const EVENT_TARGET_SEPARATOR = ':'; +export const NG_CONTENT_ELEMENT_NAME = 'ng-content'; +export const NG_SHADOW_ROOT_ELEMENT_NAME = 'shadow-root'; + var CAMEL_CASE_REGEXP = /([A-Z])/g; var DASH_CASE_REGEXP = /-([a-z])/g; + export function camelCaseToDashCase(input: string): string { return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => { return '-' + m[1].toLowerCase(); }); @@ -17,3 +25,101 @@ export function dashCaseToCamelCase(input: string): string { return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => { return m[1].toUpperCase(); }); } + +// Attention: This is on the hot path, so don't use closures or default values! +export function queryBoundElements(templateContent: Node, isSingleElementChild: boolean): + Element[] { + var result; + var dynamicElementList; + var elementIdx = 0; + if (isSingleElementChild) { + var rootElement = DOM.firstChild(templateContent); + var rootHasBinding = DOM.hasClass(rootElement, NG_BINDING_CLASS); + dynamicElementList = DOM.getElementsByClassName(rootElement, NG_BINDING_CLASS); + result = ListWrapper.createFixedSize(dynamicElementList.length + (rootHasBinding ? 1 : 0)); + if (rootHasBinding) { + result[elementIdx++] = rootElement; + } + } else { + dynamicElementList = DOM.querySelectorAll(templateContent, NG_BINDING_CLASS_SELECTOR); + result = ListWrapper.createFixedSize(dynamicElementList.length); + } + for (var i = 0; i < dynamicElementList.length; i++) { + result[elementIdx++] = dynamicElementList[i]; + } + return result; +} + +export class ClonedProtoView { + constructor(public original: DomProtoView, public fragments: Node[][], + public boundElements: Element[], public boundTextNodes: Node[]) {} +} + +export function cloneAndQueryProtoView(pv: DomProtoView, importIntoDocument: boolean): + ClonedProtoView { + var templateContent = importIntoDocument ? DOM.importIntoDoc(DOM.content(pv.rootElement)) : + DOM.clone(DOM.content(pv.rootElement)); + + var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment); + var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements, + pv.elementBinders, pv.boundTextNodeCount); + + var fragments = queryFragments(templateContent, pv.fragmentsRootNodeCount); + return new ClonedProtoView(pv, fragments, boundElements, boundTextNodes); +} + +function queryFragments(templateContent: Node, fragmentsRootNodeCount: number[]): Node[][] { + var fragments = ListWrapper.createGrowableSize(fragmentsRootNodeCount.length); + + // Note: An explicit loop is the fastest way to convert a DOM array into a JS array! + var childNode = DOM.firstChild(templateContent); + for (var fragmentIndex = 0; fragmentIndex < fragments.length; fragmentIndex++) { + var fragment = ListWrapper.createFixedSize(fragmentsRootNodeCount[fragmentIndex]); + fragments[fragmentIndex] = fragment; + for (var i = 0; i < fragment.length; i++) { + fragment[i] = childNode; + childNode = DOM.nextSibling(childNode); + } + } + return fragments; +} + +function queryBoundTextNodes(templateContent: Node, rootTextNodeIndices: number[], + boundElements: Element[], elementBinders: DomElementBinder[], + boundTextNodeCount: number): Node[] { + var boundTextNodes = ListWrapper.createFixedSize(boundTextNodeCount); + var textNodeIndex = 0; + if (rootTextNodeIndices.length > 0) { + var rootChildNodes = DOM.childNodes(templateContent); + for (var i = 0; i < rootTextNodeIndices.length; i++) { + boundTextNodes[textNodeIndex++] = rootChildNodes[rootTextNodeIndices[i]]; + } + } + for (var i = 0; i < elementBinders.length; i++) { + var binder = elementBinders[i]; + var element: Node = boundElements[i]; + if (binder.textNodeIndices.length > 0) { + var childNodes = DOM.childNodes(element); + for (var j = 0; j < binder.textNodeIndices.length; j++) { + boundTextNodes[textNodeIndex++] = childNodes[binder.textNodeIndices[j]]; + } + } + } + return boundTextNodes; +} + + +export function isElementWithTag(node: Node, elementName: string): boolean { + return DOM.isElementNode(node) && DOM.tagName(node).toLowerCase() == elementName.toLowerCase(); +} + +export function queryBoundTextNodeIndices(parentNode: Node, boundTextNodes: Map, + resultCallback: Function) { + var childNodes = DOM.childNodes(parentNode); + for (var j = 0; j < childNodes.length; j++) { + var node = childNodes[j]; + if (boundTextNodes.has(node)) { + resultCallback(node, j, boundTextNodes.get(node)); + } + } +} \ No newline at end of file diff --git a/modules/angular2/src/render/dom/view/element.ts b/modules/angular2/src/render/dom/view/element.ts deleted file mode 100644 index dd237a3c5e..0000000000 --- a/modules/angular2/src/render/dom/view/element.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {ElementBinder} from './element_binder'; -import {DomViewContainer} from './view_container'; -import {LightDom} from '../shadow_dom/light_dom'; -import {Content} from '../shadow_dom/content_tag'; - -export class DomElement { - viewContainer: DomViewContainer; - lightDom: LightDom; - constructor(public proto: ElementBinder, public element: any /* element */, - public contentTag: Content) {} -} diff --git a/modules/angular2/src/render/dom/view/element_binder.ts b/modules/angular2/src/render/dom/view/element_binder.ts index ba0df970fe..29c721754b 100644 --- a/modules/angular2/src/render/dom/view/element_binder.ts +++ b/modules/angular2/src/render/dom/view/element_binder.ts @@ -1,42 +1,29 @@ import {AST} from 'angular2/change_detection'; import {List, ListWrapper} from 'angular2/src/facade/collection'; -import * as protoViewModule from './proto_view'; -export class ElementBinder { - contentTagSelector: string; +export class DomElementBinder { textNodeIndices: List; - nestedProtoView: protoViewModule.DomProtoView; + hasNestedProtoView: boolean; eventLocals: AST; localEvents: List; globalEvents: List; - componentId: string; - parentIndex: number; - distanceToParent: number; - elementIsEmpty: boolean; + hasNativeShadowRoot: boolean; - constructor({textNodeIndices, contentTagSelector, nestedProtoView, componentId, eventLocals, - localEvents, globalEvents, parentIndex, distanceToParent, elementIsEmpty}: { - contentTagSelector?: string, + constructor({textNodeIndices, hasNestedProtoView, eventLocals, localEvents, globalEvents, + hasNativeShadowRoot}: { textNodeIndices?: List, - nestedProtoView?: protoViewModule.DomProtoView, + hasNestedProtoView?: boolean, eventLocals?: AST, localEvents?: List, globalEvents?: List, - componentId?: string, - parentIndex?: number, - distanceToParent?: number, - elementIsEmpty?: boolean + hasNativeShadowRoot?: boolean } = {}) { this.textNodeIndices = textNodeIndices; - this.contentTagSelector = contentTagSelector; - this.nestedProtoView = nestedProtoView; - this.componentId = componentId; + this.hasNestedProtoView = hasNestedProtoView; this.eventLocals = eventLocals; this.localEvents = localEvents; this.globalEvents = globalEvents; - this.parentIndex = parentIndex; - this.distanceToParent = distanceToParent; - this.elementIsEmpty = elementIsEmpty; + this.hasNativeShadowRoot = hasNativeShadowRoot; } } diff --git a/modules/angular2/src/render/dom/view/fragment.ts b/modules/angular2/src/render/dom/view/fragment.ts new file mode 100644 index 0000000000..892e3cbe4e --- /dev/null +++ b/modules/angular2/src/render/dom/view/fragment.ts @@ -0,0 +1,9 @@ +import {RenderFragmentRef} from '../../api'; + +export function resolveInternalDomFragment(fragmentRef: RenderFragmentRef): Node[] { + return (fragmentRef)._nodes; +} + +export class DomFragmentRef extends RenderFragmentRef { + constructor(public _nodes: Node[]) { super(); } +} diff --git a/modules/angular2/src/render/dom/view/proto_view.ts b/modules/angular2/src/render/dom/view/proto_view.ts index ae1a338426..ee78a5a5fe 100644 --- a/modules/angular2/src/render/dom/view/proto_view.ts +++ b/modules/angular2/src/render/dom/view/proto_view.ts @@ -1,12 +1,10 @@ -import {isPresent} from 'angular2/src/facade/lang'; -import {DOM} from 'angular2/src/dom/dom_adapter'; - +import {isBlank} from 'angular2/src/facade/lang'; import {List, ListWrapper} from 'angular2/src/facade/collection'; -import {ElementBinder} from './element_binder'; -import {NG_BINDING_CLASS} from '../util'; +import {DomElementBinder} from './element_binder'; +import {RenderProtoViewRef, ViewType} from '../../api'; -import {RenderProtoViewRef} from '../../api'; +import {DOM} from 'angular2/src/dom/dom_adapter'; export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView { return (protoViewRef)._protoView; @@ -17,24 +15,40 @@ export class DomProtoViewRef extends RenderProtoViewRef { } export class DomProtoView { - element; - elementBinders: List; - isTemplateElement: boolean; - rootBindingOffset: number; - // the number of content tags seen in this or any child proto view. - transitiveContentTagCount: number; - boundTextNodeCount: number; - rootNodeCount: number; - - constructor({elementBinders, element, transitiveContentTagCount, boundTextNodeCount}) { - this.element = element; - this.elementBinders = elementBinders; - this.transitiveContentTagCount = transitiveContentTagCount; - this.isTemplateElement = DOM.isTemplateElement(this.element); - this.rootBindingOffset = - (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0; - this.boundTextNodeCount = boundTextNodeCount; - this.rootNodeCount = - this.isTemplateElement ? DOM.childNodes(DOM.content(this.element)).length : 1; + static create(type: ViewType, rootElement: Element, fragmentsRootNodeCount: number[], + rootTextNodeIndices: number[], elementBinders: List, + mappedElementIndices: number[], mappedTextIndices: number[], + hostElementIndicesByViewIndex: number[]): DomProtoView { + var boundTextNodeCount = rootTextNodeIndices.length; + for (var i = 0; i < elementBinders.length; i++) { + boundTextNodeCount += elementBinders[i].textNodeIndices.length; + } + if (isBlank(mappedElementIndices)) { + mappedElementIndices = ListWrapper.createFixedSize(elementBinders.length); + for (var i = 0; i < mappedElementIndices.length; i++) { + mappedElementIndices[i] = i; + } + } + if (isBlank(mappedTextIndices)) { + mappedTextIndices = ListWrapper.createFixedSize(boundTextNodeCount); + for (var i = 0; i < mappedTextIndices.length; i++) { + mappedTextIndices[i] = i; + } + } + if (isBlank(hostElementIndicesByViewIndex)) { + hostElementIndicesByViewIndex = [null]; + } + var isSingleElementFragment = fragmentsRootNodeCount.length === 1 && + fragmentsRootNodeCount[0] === 1 && + DOM.isElementNode(DOM.firstChild(DOM.content(rootElement))); + return new DomProtoView(type, rootElement, elementBinders, rootTextNodeIndices, + boundTextNodeCount, fragmentsRootNodeCount, isSingleElementFragment, + mappedElementIndices, mappedTextIndices, hostElementIndicesByViewIndex); } + + constructor(public type: ViewType, public rootElement: Element, + public elementBinders: List, public rootTextNodeIndices: number[], + public boundTextNodeCount: number, public fragmentsRootNodeCount: number[], + public isSingleElementFragment: boolean, public mappedElementIndices: number[], + public mappedTextIndices: number[], public hostElementIndicesByViewIndex: number[]) {} } diff --git a/modules/angular2/src/render/dom/view/proto_view_builder.ts b/modules/angular2/src/render/dom/view/proto_view_builder.ts index d8dbcda48f..49169c16d6 100644 --- a/modules/angular2/src/render/dom/view/proto_view_builder.ts +++ b/modules/angular2/src/render/dom/view/proto_view_builder.ts @@ -19,17 +19,19 @@ import { } from 'angular2/change_detection'; import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view'; -import {ElementBinder, Event, HostAction} from './element_binder'; +import {DomElementBinder, Event, HostAction} from './element_binder'; import * as api from '../../api'; -import {NG_BINDING_CLASS, EVENT_TARGET_SEPARATOR} from '../util'; +import {NG_BINDING_CLASS, EVENT_TARGET_SEPARATOR, queryBoundTextNodeIndices} from '../util'; export class ProtoViewBuilder { variableBindings: Map = new Map(); elements: List = []; + rootTextBindings: Map = new Map(); - constructor(public rootElement, public type: api.ViewType) {} + constructor(public rootElement, public type: api.ViewType, + public useNativeShadowDom: boolean = false) {} bindElement(element, description = null): ElementBinderBuilder { var builder = new ElementBinderBuilder(this.elements.length, element, description); @@ -49,12 +51,22 @@ export class ProtoViewBuilder { this.variableBindings.set(value, name); } + // Note: We don't store the node index until the compilation is complete, + // as the compiler might change the order of elements. + bindRootText(textNode, expression) { this.rootTextBindings.set(textNode, expression); } + build(): api.ProtoViewDto { - var renderElementBinders = []; + var domElementBinders = []; var apiElementBinders = []; - var transitiveContentTagCount = 0; - var boundTextNodeCount = 0; + var textNodeExpressions = []; + var rootTextNodeIndices = []; + queryBoundTextNodeIndices(DOM.content(this.rootElement), this.rootTextBindings, + (node, nodeIndex, expression) => { + textNodeExpressions.push(expression); + rootTextNodeIndices.push(nodeIndex); + }); + ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => { var directiveTemplatePropertyNames = new Set(); var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb: DirectiveBuilder) => { @@ -71,15 +83,12 @@ export class ProtoViewBuilder { }); }); var nestedProtoView = isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null; - var nestedRenderProtoView = - isPresent(nestedProtoView) ? resolveInternalDomProtoView(nestedProtoView.render) : null; - if (isPresent(nestedRenderProtoView)) { - transitiveContentTagCount += nestedRenderProtoView.transitiveContentTagCount; - } - if (isPresent(ebb.contentTagSelector)) { - transitiveContentTagCount++; - } var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1; + var textNodeIndices = []; + queryBoundTextNodeIndices(ebb.element, ebb.textBindings, (node, nodeIndex, expression) => { + textNodeExpressions.push(expression); + textNodeIndices.push(nodeIndex); + }); apiElementBinders.push(new api.ElementBinder({ index: ebb.index, parentIndex: parentIndex, @@ -91,64 +100,28 @@ export class ProtoViewBuilder { ebb.propertyBindings, directiveTemplatePropertyNames), variableBindings: ebb.variableBindings, eventBindings: ebb.eventBindings, - textBindings: ebb.textBindings, readAttributes: ebb.readAttributes })); - var childNodeInfo = this._analyzeChildNodes(ebb.element, ebb.textBindingNodes); - boundTextNodeCount += ebb.textBindingNodes.length; - renderElementBinders.push(new ElementBinder({ - textNodeIndices: childNodeInfo.boundTextNodeIndices, - contentTagSelector: ebb.contentTagSelector, - parentIndex: parentIndex, - distanceToParent: ebb.distanceToParent, - nestedProtoView: - isPresent(nestedProtoView) ? resolveInternalDomProtoView(nestedProtoView.render) : null, - componentId: ebb.componentId, + domElementBinders.push(new DomElementBinder({ + textNodeIndices: textNodeIndices, + hasNestedProtoView: isPresent(nestedProtoView) || isPresent(ebb.componentId), + hasNativeShadowRoot: isPresent(ebb.componentId) && this.useNativeShadowDom, eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()), localEvents: ebb.eventBuilder.buildLocalEvents(), - globalEvents: ebb.eventBuilder.buildGlobalEvents(), - elementIsEmpty: childNodeInfo.elementIsEmpty + globalEvents: ebb.eventBuilder.buildGlobalEvents() })); }); + var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length; return new api.ProtoViewDto({ - render: new DomProtoViewRef(new DomProtoView({ - element: this.rootElement, - elementBinders: renderElementBinders, - transitiveContentTagCount: transitiveContentTagCount, - boundTextNodeCount: boundTextNodeCount - })), + render: new DomProtoViewRef(DomProtoView.create(this.type, this.rootElement, [rootNodeCount], + rootTextNodeIndices, domElementBinders, null, + null, null)), type: this.type, elementBinders: apiElementBinders, - variableBindings: this.variableBindings + variableBindings: this.variableBindings, + textBindings: textNodeExpressions }); } - - // Note: We need to calculate the next node indices not until the compilation is complete, - // as the compiler might change the order of elements. - private _analyzeChildNodes(parentElement: /*element*/ any, - boundTextNodes: List): _ChildNodesInfo { - var childNodes = DOM.childNodes(DOM.templateAwareRoot(parentElement)); - var boundTextNodeIndices = []; - var indexInBoundTextNodes = 0; - var elementIsEmpty = true; - for (var i = 0; i < childNodes.length; i++) { - var node = childNodes[i]; - if (indexInBoundTextNodes < boundTextNodes.length && - node === boundTextNodes[indexInBoundTextNodes]) { - boundTextNodeIndices.push(i); - indexInBoundTextNodes++; - elementIsEmpty = false; - } else if ((DOM.isTextNode(node) && DOM.getText(node).trim().length > 0) || - (DOM.isElementNode(node))) { - elementIsEmpty = false; - } - } - return new _ChildNodesInfo(boundTextNodeIndices, elementIsEmpty); - } -} - -class _ChildNodesInfo { - constructor(public boundTextNodeIndices: List, public elementIsEmpty: boolean) {} } export class ElementBinderBuilder { @@ -161,9 +134,7 @@ export class ElementBinderBuilder { propertyBindingsToDirectives: Set = new Set(); eventBindings: List = []; eventBuilder: EventBuilder = new EventBuilder(); - textBindingNodes: List = []; - textBindings: List = []; - contentTagSelector: string = null; + textBindings: Map = new Map(); readAttributes: Map = new Map(); componentId: string = null; @@ -229,12 +200,9 @@ export class ElementBinderBuilder { this.eventBindings.push(this.eventBuilder.add(name, expression, target)); } - bindText(textNode, expression) { - this.textBindingNodes.push(textNode); - this.textBindings.push(expression); - } - - setContentTagSelector(value: string) { this.contentTagSelector = value; } + // Note: We don't store the node index until the compilation is complete, + // as the compiler might change the order of elements. + bindText(textNode, expression) { this.textBindings.set(textNode, expression); } setComponentId(componentId: string) { this.componentId = componentId; } } diff --git a/modules/angular2/src/render/dom/view/proto_view_merger.ts b/modules/angular2/src/render/dom/view/proto_view_merger.ts new file mode 100644 index 0000000000..890d51b729 --- /dev/null +++ b/modules/angular2/src/render/dom/view/proto_view_merger.ts @@ -0,0 +1,451 @@ +import {DOM} from 'angular2/src/dom/dom_adapter'; +import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang'; + +import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view'; +import {DomElementBinder} from './element_binder'; +import {RenderProtoViewMergeMapping, RenderProtoViewRef, ViewType} from '../../api'; +import { + NG_BINDING_CLASS, + NG_CONTENT_ELEMENT_NAME, + ClonedProtoView, + cloneAndQueryProtoView, + queryBoundElements, + queryBoundTextNodeIndices, + NG_SHADOW_ROOT_ELEMENT_NAME, + isElementWithTag +} from '../util'; +import {CssSelector} from '../compiler/selector'; + +const NOT_MATCHABLE_SELECTOR = '_not-matchable_'; + +export function mergeProtoViewsRecursively(protoViewRefs: List>): + RenderProtoViewMergeMapping[] { + var target = []; + _mergeProtoViewsRecursively(protoViewRefs, target); + return target; +} + +function _mergeProtoViewsRecursively(protoViewRefs: List>, + target: RenderProtoViewMergeMapping[]): RenderProtoViewRef { + var targetIndex = target.length; + target.push(null); + + var resolvedProtoViewRefs = protoViewRefs.map((entry) => { + if (isArray(entry)) { + return _mergeProtoViewsRecursively(>entry, target); + } else { + return entry; + } + }); + var mapping = mergeProtoViews(resolvedProtoViewRefs); + target[targetIndex] = mapping; + return mapping.mergedProtoViewRef; +} + +export function mergeProtoViews(protoViewRefs: RenderProtoViewRef[]): RenderProtoViewMergeMapping { + var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]); + + var mergeableProtoViews: DomProtoView[] = []; + var hostElementIndices: number[] = []; + + mergeableProtoViews.push(hostProtoView); + var protoViewIdx = 1; + for (var i = 0; i < hostProtoView.elementBinders.length; i++) { + var binder = hostProtoView.elementBinders[i]; + if (binder.hasNestedProtoView) { + var nestedProtoViewRef = protoViewRefs[protoViewIdx++]; + if (isPresent(nestedProtoViewRef)) { + mergeableProtoViews.push(resolveInternalDomProtoView(nestedProtoViewRef)); + hostElementIndices.push(i); + } + } + } + return _mergeProtoViews(mergeableProtoViews, hostElementIndices); +} + + +function _mergeProtoViews(mergeableProtoViews: DomProtoView[], hostElementIndices: number[]): + RenderProtoViewMergeMapping { + var clonedProtoViews: ClonedProtoView[] = + mergeableProtoViews.map(domProtoView => cloneAndQueryProtoView(domProtoView, false)); + var hostProtoView: ClonedProtoView = clonedProtoViews[0]; + + // modify the DOM + mergeDom(clonedProtoViews, hostElementIndices); + + // create a new root element with the changed fragments and elements + var rootElement = createRootElementFromFragments(hostProtoView.fragments); + var fragmentsRootNodeCount = hostProtoView.fragments.map(fragment => fragment.length); + var rootNode = DOM.content(rootElement); + + // read out the new element / text node / ElementBinder order + var mergedBoundElements = queryBoundElements(rootNode, false); + var mergedBoundTextIndices: Map = new Map(); + var boundTextNodeMap: Map = indexBoundTextNodes(clonedProtoViews); + var rootTextNodeIndices = + calcRootTextNodeIndices(rootNode, boundTextNodeMap, mergedBoundTextIndices); + var mergedElementBinders = calcElementBinders(clonedProtoViews, mergedBoundElements, + boundTextNodeMap, mergedBoundTextIndices); + + // create element / text index mappings + var mappedElementIndices = calcMappedElementIndices(clonedProtoViews, mergedBoundElements); + var mappedTextIndices = calcMappedTextIndices(clonedProtoViews, mergedBoundTextIndices); + var hostElementIndicesByViewIndex = + calcHostElementIndicesByViewIndex(clonedProtoViews, hostElementIndices); + + // create result + var mergedProtoView = DomProtoView.create( + hostProtoView.original.type, rootElement, fragmentsRootNodeCount, rootTextNodeIndices, + mergedElementBinders, mappedElementIndices, mappedTextIndices, hostElementIndicesByViewIndex); + return new RenderProtoViewMergeMapping(new DomProtoViewRef(mergedProtoView), + fragmentsRootNodeCount.length, mappedElementIndices, + mappedTextIndices, hostElementIndicesByViewIndex); +} + +function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map { + var boundTextNodeMap = new Map(); + for (var pvIndex = 0; pvIndex < mergableProtoViews.length; pvIndex++) { + var mergableProtoView = mergableProtoViews[pvIndex]; + mergableProtoView.boundTextNodes.forEach( + (textNode) => { boundTextNodeMap.set(textNode, null); }); + } + return boundTextNodeMap; +} + +function mergeDom(clonedProtoViews: ClonedProtoView[], hostElementIndices: number[]) { + var nestedProtoViewByHostElement: Map = + indexProtoViewsByHostElement(clonedProtoViews, hostElementIndices); + + var hostProtoView = clonedProtoViews[0]; + var mergableProtoViewIdx = 1; + hostElementIndices.forEach((boundElementIndex) => { + var binder = hostProtoView.original.elementBinders[boundElementIndex]; + if (binder.hasNestedProtoView) { + var mergableNestedProtoView: ClonedProtoView = clonedProtoViews[mergableProtoViewIdx++]; + if (mergableNestedProtoView.original.type === ViewType.COMPONENT) { + mergeComponentDom(hostProtoView, boundElementIndex, mergableNestedProtoView, + nestedProtoViewByHostElement); + } else { + mergeEmbeddedDom(hostProtoView, mergableNestedProtoView); + } + } + }); +} + +function indexProtoViewsByHostElement(mergableProtoViews: ClonedProtoView[], + hostElementIndices: number[]): Map { + var hostProtoView = mergableProtoViews[0]; + var mergableProtoViewIdx = 1; + var nestedProtoViewByHostElement: Map = new Map(); + hostElementIndices.forEach((hostElementIndex) => { + nestedProtoViewByHostElement.set(hostProtoView.boundElements[hostElementIndex], + mergableProtoViews[mergableProtoViewIdx++]); + }); + return nestedProtoViewByHostElement; +} + +function mergeComponentDom(hostProtoView: ClonedProtoView, boundElementIndex: number, + nestedProtoView: ClonedProtoView, + nestedProtoViewByHostElement: Map) { + var hostElement = hostProtoView.boundElements[boundElementIndex]; + + // We wrap the fragments into elements so that we can expand + // even for root nodes in the fragment without special casing them. + var fragmentElements = mapFragmentsIntoElements(nestedProtoView.fragments); + var contentElements = findContentElements(fragmentElements); + + var projectableNodes = DOM.childNodesAsList(hostElement); + for (var i = 0; i < contentElements.length; i++) { + var contentElement = contentElements[i]; + var select = DOM.getAttribute(contentElement, 'select'); + projectableNodes = projectMatchingNodes(select, contentElement, projectableNodes); + } + + // unwrap the fragment elements into arrays of nodes after projecting + var fragments = extractFragmentNodesFromElements(fragmentElements); + appendComponentNodesToHost(hostProtoView, boundElementIndex, fragments[0]); + + for (var i = 1; i < fragments.length; i++) { + hostProtoView.fragments.push(fragments[i]); + } +} + +function mapFragmentsIntoElements(fragments: Node[][]): Element[] { + return fragments.map((fragment) => { + var fragmentElement = DOM.createTemplate(''); + fragment.forEach(node => DOM.appendChild(DOM.content(fragmentElement), node)); + return fragmentElement; + }); +} + +function extractFragmentNodesFromElements(fragmentElements: Element[]): Node[][] { + return fragmentElements.map( + (fragmentElement) => { return DOM.childNodesAsList(DOM.content(fragmentElement)); }); +} + +function findContentElements(fragmentElements: Element[]): Element[] { + var contentElements = []; + fragmentElements.forEach((fragmentElement: Element) => { + var fragmentContentElements = + DOM.querySelectorAll(DOM.content(fragmentElement), NG_CONTENT_ELEMENT_NAME); + for (var i = 0; i < fragmentContentElements.length; i++) { + contentElements.push(fragmentContentElements[i]); + } + }); + return sortContentElements(contentElements); +} + +function appendComponentNodesToHost(hostProtoView: ClonedProtoView, boundElementIndex: number, + componentRootNodes: Node[]) { + var hostElement = hostProtoView.boundElements[boundElementIndex]; + var binder = hostProtoView.original.elementBinders[boundElementIndex]; + if (binder.hasNativeShadowRoot) { + var shadowRootWrapper = DOM.createElement(NG_SHADOW_ROOT_ELEMENT_NAME); + for (var i = 0; i < componentRootNodes.length; i++) { + DOM.appendChild(shadowRootWrapper, componentRootNodes[i]); + } + var firstChild = DOM.firstChild(hostElement); + if (isPresent(firstChild)) { + DOM.insertBefore(firstChild, shadowRootWrapper); + } else { + DOM.appendChild(hostElement, shadowRootWrapper); + } + } else { + DOM.clearNodes(hostElement); + for (var i = 0; i < componentRootNodes.length; i++) { + DOM.appendChild(hostElement, componentRootNodes[i]); + } + } +} + +function mergeEmbeddedDom(parentProtoView: ClonedProtoView, nestedProtoView: ClonedProtoView) { + nestedProtoView.fragments.forEach((fragment) => parentProtoView.fragments.push(fragment)); +} + +function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] { + var remaining = []; + var removeContentElement = true; + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (isWildcard(selector)) { + DOM.insertBefore(contentElement, node); + } else if (DOM.isElementNode(node)) { + if (isElementWithTag(node, NG_CONTENT_ELEMENT_NAME)) { + // keep the projected content as other elements + // might want to use it as well. + remaining.push(node); + DOM.setAttribute(contentElement, 'select', + mergeSelectors(selector, DOM.getAttribute(node, 'select'))); + removeContentElement = false; + } else { + if (DOM.elementMatches(node, selector)) { + DOM.insertBefore(contentElement, node); + } else { + remaining.push(node); + } + } + } else { + // non projected text nodes + remaining.push(node); + } + } + if (removeContentElement) { + DOM.remove(contentElement); + } + return remaining; +} + +function isWildcard(selector): boolean { + return isBlank(selector) || selector.length === 0 || selector == '*'; +} + +export function mergeSelectors(selector1: string, selector2: string): string { + if (isWildcard(selector1)) { + return isBlank(selector2) ? '' : selector2; + } else if (isWildcard(selector2)) { + return isBlank(selector1) ? '' : selector1; + } else { + var sels1 = CssSelector.parse(selector1); + var sels2 = CssSelector.parse(selector2); + if (sels1.length > 1 || sels2.length > 1) { + throw new BaseException('multiple selectors are not supported in ng-content'); + } + var sel1 = sels1[0]; + var sel2 = sels2[0]; + if (sel1.notSelectors.length > 0 || sel2.notSelectors.length > 0) { + throw new BaseException(':not selector is not supported in ng-content'); + } + var merged = new CssSelector(); + if (isBlank(sel1.element)) { + merged.setElement(sel2.element); + } else if (isBlank(sel2.element)) { + merged.setElement(sel1.element); + } else { + return NOT_MATCHABLE_SELECTOR; + } + merged.attrs = sel1.attrs.concat(sel2.attrs); + merged.classNames = sel1.classNames.concat(sel2.classNames); + return merged.toString(); + } +} + +// we need to sort content elements as they can originate from +// different sub views +function sortContentElements(contentElements: Element[]): Element[] { + // for now, only move the wildcard selector to the end. + // TODO(tbosch): think about sorting by selector specifity... + var firstWildcard = null; + var sorted = []; + contentElements.forEach((contentElement) => { + var select = DOM.getAttribute(contentElement, 'select'); + if (isWildcard(select)) { + if (isBlank(firstWildcard)) { + firstWildcard = contentElement; + } + } else { + sorted.push(contentElement); + } + }); + if (isPresent(firstWildcard)) { + sorted.push(firstWildcard); + } + return sorted; +} + + +function createRootElementFromFragments(fragments: Node[][]): Element { + var rootElement = DOM.createTemplate(''); + var rootNode = DOM.content(rootElement); + fragments.forEach( + (fragment) => { fragment.forEach((node) => { DOM.appendChild(rootNode, node); }); }); + return rootElement; +} + +function calcRootTextNodeIndices(rootNode: Node, boundTextNodes: Map, + targetBoundTextIndices: Map): number[] { + var rootTextNodeIndices = []; + queryBoundTextNodeIndices(rootNode, boundTextNodes, (textNode, nodeIndex, _) => { + rootTextNodeIndices.push(nodeIndex); + targetBoundTextIndices.set(textNode, targetBoundTextIndices.size); + }); + return rootTextNodeIndices; +} + +function calcElementBinders(clonedProtoViews: ClonedProtoView[], mergedBoundElements: Element[], + boundTextNodes: Map, + targetBoundTextIndices: Map): DomElementBinder[] { + var elementBinderByElement: Map = + indexElementBindersByElement(clonedProtoViews); + var mergedElementBinders = []; + for (var i = 0; i < mergedBoundElements.length; i++) { + var element = mergedBoundElements[i]; + var textNodeIndices = []; + queryBoundTextNodeIndices(element, boundTextNodes, (textNode, nodeIndex, _) => { + textNodeIndices.push(nodeIndex); + targetBoundTextIndices.set(textNode, targetBoundTextIndices.size); + }); + mergedElementBinders.push( + updateElementBinderTextNodeIndices(elementBinderByElement.get(element), textNodeIndices)); + } + return mergedElementBinders; +} + +function indexElementBindersByElement(mergableProtoViews: ClonedProtoView[]): + Map { + var elementBinderByElement = new Map(); + mergableProtoViews.forEach((mergableProtoView) => { + for (var i = 0; i < mergableProtoView.boundElements.length; i++) { + var el = mergableProtoView.boundElements[i]; + if (isPresent(el)) { + elementBinderByElement.set(el, mergableProtoView.original.elementBinders[i]); + } + } + }); + return elementBinderByElement; +} + +function updateElementBinderTextNodeIndices(elementBinder: DomElementBinder, + textNodeIndices: number[]): DomElementBinder { + var result; + if (isBlank(elementBinder)) { + result = new DomElementBinder({ + textNodeIndices: textNodeIndices, + hasNestedProtoView: false, + eventLocals: null, + localEvents: [], + globalEvents: [], + hasNativeShadowRoot: null + }); + } else { + result = new DomElementBinder({ + textNodeIndices: textNodeIndices, + hasNestedProtoView: false, + eventLocals: elementBinder.eventLocals, + localEvents: elementBinder.localEvents, + globalEvents: elementBinder.globalEvents, + hasNativeShadowRoot: elementBinder.hasNativeShadowRoot + }); + } + return result; +} + +function calcMappedElementIndices(clonedProtoViews: ClonedProtoView[], + mergedBoundElements: Element[]): number[] { + var mergedBoundElementIndices: Map = indexArray(mergedBoundElements); + var mappedElementIndices = []; + clonedProtoViews.forEach((clonedProtoView) => { + clonedProtoView.original.mappedElementIndices.forEach((boundElementIndex) => { + var mappedElementIndex = null; + if (isPresent(boundElementIndex)) { + var boundElement = clonedProtoView.boundElements[boundElementIndex]; + mappedElementIndex = mergedBoundElementIndices.get(boundElement); + } + mappedElementIndices.push(mappedElementIndex); + }); + }); + return mappedElementIndices; +} + +function calcMappedTextIndices(clonedProtoViews: ClonedProtoView[], + mergedBoundTextIndices: Map): number[] { + var mappedTextIndices = []; + clonedProtoViews.forEach((clonedProtoView) => { + clonedProtoView.original.mappedTextIndices.forEach((textNodeIndex) => { + var mappedTextIndex = null; + if (isPresent(textNodeIndex)) { + var textNode = clonedProtoView.boundTextNodes[textNodeIndex]; + mappedTextIndex = mergedBoundTextIndices.get(textNode); + } + mappedTextIndices.push(mappedTextIndex); + }); + }); + return mappedTextIndices; +} + +function calcHostElementIndicesByViewIndex(clonedProtoViews: ClonedProtoView[], + hostElementIndices: number[]): number[] { + var mergedElementCount = 0; + var hostElementIndicesByViewIndex = []; + for (var i = 0; i < clonedProtoViews.length; i++) { + var clonedProtoView = clonedProtoViews[i]; + clonedProtoView.original.hostElementIndicesByViewIndex.forEach((hostElementIndex) => { + var mappedHostElementIndex; + if (isBlank(hostElementIndex)) { + mappedHostElementIndex = i > 0 ? hostElementIndices[i - 1] : null; + } else { + mappedHostElementIndex = hostElementIndex + mergedElementCount; + } + hostElementIndicesByViewIndex.push(mappedHostElementIndex); + }); + mergedElementCount += clonedProtoView.original.mappedElementIndices.length; + } + return hostElementIndicesByViewIndex; +} + +function indexArray(arr: any[]): Map { + var map = new Map(); + for (var i = 0; i < arr.length; i++) { + map.set(arr[i], i); + } + return map; +} diff --git a/modules/angular2/src/render/dom/view/view.ts b/modules/angular2/src/render/dom/view/view.ts index 2d86354f40..c2c704a8fb 100644 --- a/modules/angular2/src/render/dom/view/view.ts +++ b/modules/angular2/src/render/dom/view/view.ts @@ -3,10 +3,8 @@ import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src import {isPresent, isBlank, BaseException, stringify} from 'angular2/src/facade/lang'; import {DomProtoView} from './proto_view'; -import {LightDom} from '../shadow_dom/light_dom'; -import {DomElement} from './element'; -import {RenderViewRef, EventDispatcher} from '../../api'; +import {RenderViewRef, RenderEventDispatcher} from '../../api'; import {camelCaseToDashCase} from '../util'; export function resolveInternalDomView(viewRef: RenderViewRef): DomView { @@ -21,30 +19,19 @@ export class DomViewRef extends RenderViewRef { * Const of making objects: http://jsperf.com/instantiate-size-of-object */ export class DomView { - hostLightDom: LightDom = null; - shadowRoot = null; hydrated: boolean = false; - eventDispatcher: EventDispatcher = null; + eventDispatcher: RenderEventDispatcher = null; eventHandlerRemovers: List = []; - constructor(public proto: DomProtoView, public rootNodes: List, - public boundTextNodes: List, public boundElements: List) {} - - getDirectParentElement(boundElementIndex: number): DomElement { - var binder = this.proto.elementBinders[boundElementIndex]; - var parent = null; - if (binder.parentIndex !== -1 && binder.distanceToParent === 1) { - parent = this.boundElements[binder.parentIndex]; - } - return parent; - } + constructor(public proto: DomProtoView, public boundTextNodes: List, + public boundElements: Element[]) {} setElementProperty(elementIndex: number, propertyName: string, value: any) { - DOM.setProperty(this.boundElements[elementIndex].element, propertyName, value); + DOM.setProperty(this.boundElements[elementIndex], propertyName, value); } setElementAttribute(elementIndex: number, attributeName: string, value: string) { - var element = this.boundElements[elementIndex].element; + var element = this.boundElements[elementIndex]; var dashCasedAttributeName = camelCaseToDashCase(attributeName); if (isPresent(value)) { DOM.setAttribute(element, dashCasedAttributeName, stringify(value)); @@ -54,7 +41,7 @@ export class DomView { } setElementClass(elementIndex: number, className: string, isAdd: boolean) { - var element = this.boundElements[elementIndex].element; + var element = this.boundElements[elementIndex]; var dashCasedClassName = camelCaseToDashCase(className); if (isAdd) { DOM.addClass(element, dashCasedClassName); @@ -64,7 +51,7 @@ export class DomView { } setElementStyle(elementIndex: number, styleName: string, value: string) { - var element = this.boundElements[elementIndex].element; + var element = this.boundElements[elementIndex]; var dashCasedStyleName = camelCaseToDashCase(styleName); if (isPresent(value)) { DOM.setStyle(element, dashCasedStyleName, stringify(value)); @@ -74,7 +61,7 @@ export class DomView { } invokeElementMethod(elementIndex: number, methodName: string, args: List) { - var element = this.boundElements[elementIndex].element; + var element = this.boundElements[elementIndex]; DOM.invoke(element, methodName, args); } @@ -91,7 +78,7 @@ export class DomView { // Locals(null, evalLocals)); // this.eventDispatcher.dispatchEvent(elementIndex, eventName, localValues); allowDefaultBehavior = - this.eventDispatcher.dispatchEvent(elementIndex, eventName, evalLocals); + this.eventDispatcher.dispatchRenderEvent(elementIndex, eventName, evalLocals); if (!allowDefaultBehavior) { event.preventDefault(); } diff --git a/modules/angular2/src/render/dom/view/view_container.ts b/modules/angular2/src/render/dom/view/view_container.ts index c73c3d9ecd..c0a236939b 100644 --- a/modules/angular2/src/render/dom/view/view_container.ts +++ b/modules/angular2/src/render/dom/view/view_container.ts @@ -5,14 +5,4 @@ import * as viewModule from './view'; export class DomViewContainer { // The order in this list matches the DOM order. views: List = []; - - contentTagContainers(): List { return this.views; } - - nodes(): List { - var r = []; - for (var i = 0; i < this.views.length; ++i) { - r = ListWrapper.concat(r, this.views[i].rootNodes); - } - return r; - } } diff --git a/modules/angular2/src/test_lib/test_lib.ts b/modules/angular2/src/test_lib/test_lib.ts index c5e597a8b0..cb1c9934d9 100644 --- a/modules/angular2/src/test_lib/test_lib.ts +++ b/modules/angular2/src/test_lib/test_lib.ts @@ -271,6 +271,8 @@ export interface GuinessCompatibleSpy extends jasmine.Spy { /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied * function. */ andCallFake(fn: Function): GuinessCompatibleSpy; + /** removes all recorded calls */ + reset(); } export class SpyObject { @@ -320,6 +322,7 @@ export class SpyObject { var newSpy: GuinessCompatibleSpy = jasmine.createSpy(name); newSpy.andCallFake = newSpy.and.callFake; newSpy.andReturn = newSpy.and.returnValue; + newSpy.reset = newSpy.calls.reset; // return null by default to satisfy our rtts asserts newSpy.and.returnValue(null); return newSpy; diff --git a/modules/angular2/src/test_lib/utils.ts b/modules/angular2/src/test_lib/utils.ts index 4bbdc3cbf3..fc198a3655 100644 --- a/modules/angular2/src/test_lib/utils.ts +++ b/modules/angular2/src/test_lib/utils.ts @@ -1,7 +1,6 @@ import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {DOM} from 'angular2/src/dom/dom_adapter'; import {isPresent, isString, RegExpWrapper, StringWrapper, RegExp} from 'angular2/src/facade/lang'; -import {resolveInternalDomView} from 'angular2/src/render/dom/view/view'; export class Log { _result: List; @@ -17,21 +16,6 @@ export class Log { result(): string { return ListWrapper.join(this._result, "; "); } } -export function viewRootNodes(view): List { - return resolveInternalDomView(view.render).rootNodes; -} - -export function queryView(view, selector: string): any { - var rootNodes = viewRootNodes(view); - for (var i = 0; i < rootNodes.length; ++i) { - var res = DOM.querySelector(rootNodes[i], selector); - if (isPresent(res)) { - return res; - } - } - return null; -} - export function dispatchEvent(element, eventType) { DOM.dispatchEvent(element, DOM.createEvent(eventType)); } diff --git a/modules/angular2/src/web-workers/shared/serializer.ts b/modules/angular2/src/web-workers/shared/serializer.ts index ca9c6f6937..85f6e17d2f 100644 --- a/modules/angular2/src/web-workers/shared/serializer.ts +++ b/modules/angular2/src/web-workers/shared/serializer.ts @@ -185,7 +185,6 @@ class ElementBinderSerializer { 'propertyBindings': Serializer.serialize(binder.propertyBindings, ElementPropertyBinding), 'variableBindings': Serializer.mapToObject(binder.variableBindings), 'eventBindings': Serializer.serialize(binder.eventBindings, EventBinding), - 'textBindings': Serializer.serialize(binder.textBindings, ASTWithSource), 'readAttributes': Serializer.mapToObject(binder.readAttributes) }; } @@ -200,7 +199,6 @@ class ElementBinderSerializer { propertyBindings: Serializer.deserialize(obj.propertyBindings, ElementPropertyBinding), variableBindings: Serializer.objectToMap(obj.variableBindings), eventBindings: Serializer.deserialize(obj.eventBindings, EventBinding), - textBindings: Serializer.deserialize(obj.textBindings, ASTWithSource, "interpolation"), readAttributes: Serializer.objectToMap(obj.readAttributes) }); } @@ -213,6 +211,7 @@ class ProtoViewDtoSerializer { 'render': null, 'elementBinders': Serializer.serialize(view.elementBinders, ElementBinder), 'variableBindings': Serializer.mapToObject(view.variableBindings), + 'textBindings': Serializer.serialize(view.textBindings, ASTWithSource), 'type': view.type }; } @@ -222,6 +221,7 @@ class ProtoViewDtoSerializer { render: null, // TODO: fix render refs and write a serializer for them elementBinders: Serializer.deserialize(obj.elementBinders, ElementBinder), variableBindings: Serializer.objectToMap(obj.variableBindings), + textBindings: Serializer.deserialize(obj.textBindings, ASTWithSource, "interpolation"), type: obj.type }); } diff --git a/modules/angular2/test/benchmark/transform/integration/hello_world.dart b/modules/angular2/test/benchmark/transform/integration/hello_world.dart index cd2c3d40e9..4cc3122f8c 100644 --- a/modules/angular2/test/benchmark/transform/integration/hello_world.dart +++ b/modules/angular2/test/benchmark/transform/integration/hello_world.dart @@ -48,7 +48,7 @@ import "package:angular2/di.dart" show Injectable; @Template( inline: '
{{greeting}} world!
' '', + 'change greeting', directives: const [RedDec]) class HelloCmp { String greeting; diff --git a/modules/angular2/test/core/application_spec.ts b/modules/angular2/test/core/application_spec.ts index c0af816262..f185f70e9b 100644 --- a/modules/angular2/test/core/application_spec.ts +++ b/modules/angular2/test/core/application_spec.ts @@ -29,7 +29,7 @@ class HelloRootCmp { } @Component({selector: 'hello-app'}) -@View({template: 'before: after: done'}) +@View({template: 'before: after: done'}) class HelloRootCmpContent { constructor() {} } @@ -68,19 +68,19 @@ class HelloRootDirectiveIsNotCmp { export function main() { var fakeDoc, el, el2, testBindings, lightDom; - beforeEach(() => { - fakeDoc = DOM.createHtmlDocument(); - el = DOM.createElement('hello-app', fakeDoc); - el2 = DOM.createElement('hello-app-2', fakeDoc); - lightDom = DOM.createElement('light-dom-el', fakeDoc); - DOM.appendChild(fakeDoc.body, el); - DOM.appendChild(fakeDoc.body, el2); - DOM.appendChild(el, lightDom); - DOM.setText(lightDom, 'loading'); - testBindings = [bind(DOCUMENT_TOKEN).toValue(fakeDoc)]; - }); - describe('bootstrap factory method', () => { + beforeEach(() => { + fakeDoc = DOM.createHtmlDocument(); + el = DOM.createElement('hello-app', fakeDoc); + el2 = DOM.createElement('hello-app-2', fakeDoc); + lightDom = DOM.createElement('light-dom-el', fakeDoc); + DOM.appendChild(fakeDoc.body, el); + DOM.appendChild(fakeDoc.body, el2); + DOM.appendChild(el, lightDom); + DOM.setText(lightDom, 'loading'); + testBindings = [bind(DOCUMENT_TOKEN).toValue(fakeDoc)]; + }); + it('should throw if bootstrapped Directive is not a Component', inject([AsyncTestCompleter], (async) => { var refPromise = @@ -149,14 +149,6 @@ export function main() { }); })); - it("should support shadow dom content tag", inject([AsyncTestCompleter], (async) => { - var refPromise = bootstrap(HelloRootCmpContent, testBindings); - refPromise.then((ref) => { - expect(el).toHaveText('before: loading after: done'); - async.done(); - }); - })); - it('should register each application with the testability registry', inject([AsyncTestCompleter], (async) => { var refPromise1 = bootstrap(HelloRootCmp, testBindings); diff --git a/modules/angular2/test/core/compiler/compiler_spec.ts b/modules/angular2/test/core/compiler/compiler_spec.ts index 343601f0c2..ad4af2ef81 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.ts +++ b/modules/angular2/test/core/compiler/compiler_spec.ts @@ -15,7 +15,7 @@ import { } from 'angular2/test_lib'; import {List, ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; -import {IMPLEMENTS, Type, isBlank, stringify, isPresent} from 'angular2/src/facade/lang'; +import {IMPLEMENTS, Type, isBlank, stringify, isPresent, isArray} from 'angular2/src/facade/lang'; import {PromiseWrapper, Promise} from 'angular2/src/facade/async'; import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler'; @@ -45,24 +45,41 @@ export function main() { rootProtoView; var renderCompileRequests: any[]; - beforeEach(() => { - directiveResolver = new DirectiveResolver(); - tplResolver = new FakeViewResolver(); - cmpUrlMapper = new RuntimeComponentUrlMapper(); - renderCompiler = new SpyRenderCompiler(); - renderCompiler.spy('compileHost') - .andCallFake((componentId) => { - return PromiseWrapper.resolve(createRenderProtoView( - [createRenderComponentElementBinder(0)], renderApi.ViewType.HOST)); - }); - rootProtoView = createRootProtoView(directiveResolver, MainComponent); - }); + function mergeProtoViewsRecursively( + protoViewRefs: List>, + target: renderApi.RenderProtoViewMergeMapping[]): renderApi.RenderProtoViewRef { + var targetIndex = target.length; + target.push(null); + + var flattended = protoViewRefs.map(protoViewRefOrArray => { + var resolvedProtoViewRef; + if (isArray(protoViewRefOrArray)) { + resolvedProtoViewRef = mergeProtoViewsRecursively( + >protoViewRefOrArray, target); + } else { + resolvedProtoViewRef = protoViewRefOrArray; + } + return resolvedProtoViewRef; + }); + var merged = []; + flattended.forEach((entry) => { + if (entry instanceof MergedRenderProtoViewRef) { + entry.originals.forEach(ref => merged.push(ref)); + } else { + merged.push(entry); + } + }); + var result = new MergedRenderProtoViewRef(merged); + target[targetIndex] = new renderApi.RenderProtoViewMergeMapping(result, 1, [], [], []); + return result; + } function createCompiler(renderCompileResults: List>, - protoViewFactoryResults: List>) { + protoViewFactoryResults: List) { var urlResolver = new UrlResolver(); renderCompileRequests = []; + renderCompileResults = ListWrapper.clone(renderCompileResults); renderCompiler.spy('compile').andCallFake((view) => { renderCompileRequests.push(view); return PromiseWrapper.resolve(ListWrapper.removeAt(renderCompileResults, 0)); @@ -73,12 +90,31 @@ export function main() { urlResolver, renderCompiler, protoViewFactory, new FakeAppRootUrl()); } + beforeEach(() => { + directiveResolver = new DirectiveResolver(); + tplResolver = new FakeViewResolver(); + cmpUrlMapper = new RuntimeComponentUrlMapper(); + renderCompiler = new SpyRenderCompiler(); + renderCompiler.spy('compileHost') + .andCallFake((componentId) => { + return PromiseWrapper.resolve(createRenderProtoView( + [createRenderComponentElementBinder(0)], renderApi.ViewType.HOST)); + }); + renderCompiler.spy('mergeProtoViewsRecursively') + .andCallFake((protoViewRefs: List>) => { + var result: renderApi.RenderProtoViewMergeMapping[] = []; + mergeProtoViewsRecursively(protoViewRefs, result); + return PromiseWrapper.resolve(result); + }); + rootProtoView = createRootProtoView(directiveResolver, MainComponent); + }); + describe('serialize template', () => { function captureTemplate(template: viewAnn.View): Promise { tplResolver.setView(MainComponent, template); var compiler = - createCompiler([createRenderProtoView()], [[rootProtoView], [createProtoView()]]); + createCompiler([createRenderProtoView()], [rootProtoView, createProtoView()]); return compiler.compileInHost(MainComponent) .then((_) => { expect(renderCompileRequests.length).toBe(1); @@ -260,7 +296,7 @@ export function main() { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var renderProtoView = createRenderProtoView(); var expectedProtoView = createProtoView(); - var compiler = createCompiler([renderProtoView], [[rootProtoView], [expectedProtoView]]); + var compiler = createCompiler([renderProtoView], [rootProtoView, expectedProtoView]); compiler.compileInHost(MainComponent) .then((_) => { var request = protoViewFactory.requests[1]; @@ -272,7 +308,7 @@ export function main() { it('should pass the component binding', inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var compiler = - createCompiler([createRenderProtoView()], [[rootProtoView], [createProtoView()]]); + createCompiler([createRenderProtoView()], [rootProtoView, createProtoView()]); compiler.compileInHost(MainComponent) .then((_) => { var request = protoViewFactory.requests[1]; @@ -286,7 +322,7 @@ export function main() { MainComponent, new viewAnn.View({template: '
', directives: [SomeDirective]})); var compiler = - createCompiler([createRenderProtoView()], [[rootProtoView], [createProtoView()]]); + createCompiler([createRenderProtoView()], [rootProtoView, createProtoView()]); compiler.compileInHost(MainComponent) .then((_) => { var request = protoViewFactory.requests[1]; @@ -300,7 +336,7 @@ export function main() { inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var compiler = - createCompiler([createRenderProtoView()], [[rootProtoView], [createProtoView()]]); + createCompiler([createRenderProtoView()], [rootProtoView, createProtoView()]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef)).toBe(rootProtoView); @@ -316,17 +352,21 @@ export function main() { var mainProtoView = createProtoView([createComponentElementBinder(directiveResolver, NestedComponent)]); var nestedProtoView = createProtoView(); - var compiler = createCompiler( - [ - createRenderProtoView([createRenderComponentElementBinder(0)]), - createRenderProtoView() - ], - [[rootProtoView], [mainProtoView], [nestedProtoView]]); + var renderPvDtos = [ + createRenderProtoView([createRenderComponentElementBinder(0)]), + createRenderProtoView() + ]; + var compiler = + createCompiler(renderPvDtos, [rootProtoView, mainProtoView, nestedProtoView]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef).elementBinders[0].nestedProtoView) .toBe(mainProtoView); + expect(originalRenderProtoViewRefs(mainProtoView)) + .toEqual([renderPvDtos[0].render, renderPvDtos[1].render]); expect(mainProtoView.elementBinders[0].nestedProtoView).toBe(nestedProtoView); + expect(originalRenderProtoViewRefs(nestedProtoView)) + .toEqual([renderPvDtos[1].render]); async.done(); }); })); @@ -334,25 +374,40 @@ export function main() { it('should load nested components in viewcontainers', inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); tplResolver.setView(NestedComponent, new viewAnn.View({template: '
'})); - var mainProtoView = createProtoView([createViewportElementBinder(null)]); var viewportProtoView = createProtoView([createComponentElementBinder(directiveResolver, NestedComponent)]); + var mainProtoView = createProtoView([createViewportElementBinder(viewportProtoView)]); var nestedProtoView = createProtoView(); - var compiler = createCompiler( - [ - createRenderProtoView([ - createRenderViewportElementBinder(createRenderProtoView( - [createRenderComponentElementBinder(0)], renderApi.ViewType.EMBEDDED)) - ]), - createRenderProtoView() - ], - [[rootProtoView], [mainProtoView, viewportProtoView], [nestedProtoView]]); + var renderPvDtos = [ + createRenderProtoView([ + createRenderViewportElementBinder(createRenderProtoView( + [createRenderComponentElementBinder(0)], renderApi.ViewType.EMBEDDED)) + ]), + createRenderProtoView() + ]; + var compiler = + createCompiler(renderPvDtos, [rootProtoView, mainProtoView, nestedProtoView]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef).elementBinders[0].nestedProtoView) .toBe(mainProtoView); + expect(originalRenderProtoViewRefs(mainProtoView)) + .toEqual([ + renderPvDtos[0] + .render, + renderPvDtos[0].elementBinders[0].nestedProtoView.render, + renderPvDtos[1].render + ]); expect(viewportProtoView.elementBinders[0].nestedProtoView).toBe(nestedProtoView); - + expect(originalRenderProtoViewRefs(viewportProtoView)) + .toEqual([ + renderPvDtos[0] + .elementBinders[0] + .nestedProtoView.render, + renderPvDtos[1].render + ]); + expect(originalRenderProtoViewRefs(nestedProtoView)) + .toEqual([renderPvDtos[1].render]); async.done(); }); })); @@ -360,7 +415,7 @@ export function main() { it('should cache compiled host components', inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var mainPv = createProtoView(); - var compiler = createCompiler([createRenderProtoView()], [[rootProtoView], [mainPv]]); + var compiler = createCompiler([createRenderProtoView()], [rootProtoView, mainPv]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef).elementBinders[0].nestedProtoView) @@ -404,7 +459,7 @@ export function main() { var nestedPv = createProtoView([]); var compiler = createCompiler( [createRenderProtoView(), createRenderProtoView(), createRenderProtoView()], - [[rootProtoView], [mainPv], [nestedPv], [rootProtoView2], [mainPv]]); + [rootProtoView, mainPv, nestedPv, rootProtoView2, mainPv]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef) @@ -429,7 +484,7 @@ export function main() { var renderProtoViewCompleter = PromiseWrapper.completer(); var expectedProtoView = createProtoView(); var compiler = createCompiler([renderProtoViewCompleter.promise], - [[rootProtoView], [rootProtoView], [expectedProtoView]]); + [rootProtoView, rootProtoView, expectedProtoView]); var result = PromiseWrapper.all([ compiler.compileInHost(MainComponent), compiler.compileInHost(MainComponent), @@ -445,18 +500,55 @@ export function main() { }); })); - it('should allow recursive components', inject([AsyncTestCompleter], (async) => { + it('should throw on unconditional recursive components', + inject([AsyncTestCompleter], (async) => { tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); var mainProtoView = createProtoView([createComponentElementBinder(directiveResolver, MainComponent)]); var compiler = createCompiler([createRenderProtoView([createRenderComponentElementBinder(0)])], - [[rootProtoView], [mainProtoView]]); + [rootProtoView, mainProtoView]); + PromiseWrapper.catchError(compiler.compileInHost(MainComponent), (e) => { + expect(() => { throw e; }) + .toThrowError(`Unconditional component cycle in ${stringify(MainComponent)}`); + async.done(); + return null; + }); + })); + + it('should allow recursive components that are connected via an embedded ProtoView', + inject([AsyncTestCompleter], (async) => { + tplResolver.setView(MainComponent, new viewAnn.View({template: '
'})); + var viewportProtoView = + createProtoView([createComponentElementBinder(directiveResolver, MainComponent)]); + var mainProtoView = createProtoView([createViewportElementBinder(viewportProtoView)]); + var renderPvDtos = [ + createRenderProtoView([ + createRenderViewportElementBinder(createRenderProtoView( + [createRenderComponentElementBinder(0)], renderApi.ViewType.EMBEDDED)) + ]), + createRenderProtoView() + ]; + var compiler = createCompiler(renderPvDtos, [rootProtoView, mainProtoView]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef).elementBinders[0].nestedProtoView) .toBe(mainProtoView); - expect(mainProtoView.elementBinders[0].nestedProtoView).toBe(mainProtoView); + expect(mainProtoView.elementBinders[0] + .nestedProtoView.elementBinders[0] + .nestedProtoView) + .toBe(mainProtoView); + // In case of a cycle, don't merge the embedded proto views into the component! + expect(originalRenderProtoViewRefs(mainProtoView)) + .toEqual([renderPvDtos[0].render, null]); + expect(originalRenderProtoViewRefs(viewportProtoView)) + .toEqual([ + renderPvDtos[0] + .elementBinders[0] + .nestedProtoView.render, + renderPvDtos[1].render, + null + ]); async.done(); }); })); @@ -466,8 +558,7 @@ export function main() { var rootProtoView = createProtoView([createComponentElementBinder(directiveResolver, MainComponent)]); var mainProtoView = createProtoView(); - var compiler = - createCompiler([createRenderProtoView()], [[rootProtoView], [mainProtoView]]); + var compiler = createCompiler([createRenderProtoView()], [rootProtoView, mainProtoView]); compiler.compileInHost(MainComponent) .then((protoViewRef) => { expect(internalProtoView(protoViewRef)).toBe(rootProtoView); @@ -491,7 +582,7 @@ function createDirectiveBinding(directiveResolver, type): DirectiveBinding { } function createProtoView(elementBinders = null): AppProtoView { - var pv = new AppProtoView(null, null, new Map(), null); + var pv = new AppProtoView(null, null, null, new Map(), null); if (isBlank(elementBinders)) { elementBinders = []; } @@ -518,7 +609,8 @@ function createRenderProtoView(elementBinders = null, type: renderApi.ViewType = if (isBlank(elementBinders)) { elementBinders = []; } - return new renderApi.ProtoViewDto({elementBinders: elementBinders, type: type}); + return new renderApi.ProtoViewDto( + {elementBinders: elementBinders, type: type, render: new renderApi.RenderProtoViewRef()}); } function createRenderComponentElementBinder(directiveIndex): renderApi.ElementBinder { @@ -611,14 +703,22 @@ class FakeViewResolver extends ViewResolver { class FakeProtoViewFactory extends ProtoViewFactory { requests: List>; - constructor(public results: List>) { + constructor(public results: List) { super(null); this.requests = []; } createAppProtoViews(componentBinding: DirectiveBinding, renderProtoView: renderApi.ProtoViewDto, - directives: List): List { + directives: List): AppProtoView { this.requests.push([componentBinding, renderProtoView, directives]); return ListWrapper.removeAt(this.results, 0); } } + +class MergedRenderProtoViewRef extends renderApi.RenderProtoViewRef { + constructor(public originals: renderApi.RenderProtoViewRef[]) { super(); } +} + +function originalRenderProtoViewRefs(appProtoView: AppProtoView) { + return (appProtoView.mergeMapping.renderProtoViewRef).originals; +} \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts index 7c2b503161..d0cbeb9de7 100644 --- a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts +++ b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts @@ -12,7 +12,6 @@ import { beforeEachBindings, it, xit, - viewRootNodes, TestComponentBuilder, RootTestComponent, inspectElement, @@ -240,8 +239,7 @@ export function main() { componentRef.dispose(); - expect(rootEl).toHaveText(''); - expect(rootEl.parentNode).toBe(doc.body); + expect(rootEl.parentNode).toBeFalsy(); async.done(); }); diff --git a/modules/angular2/test/core/compiler/element_injector_spec.ts b/modules/angular2/test/core/compiler/element_injector_spec.ts index 017b89a685..bfb216b35f 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.ts +++ b/modules/angular2/test/core/compiler/element_injector_spec.ts @@ -51,17 +51,10 @@ import {QueryList} from 'angular2/src/core/compiler/query_list'; @proxy @IMPLEMENTS(AppView) class DummyView extends SpyObject { - componentChildViews; changeDetector; - elementRefs; - constructor(elementCount = 0) { + constructor() { super(AppView); - this.componentChildViews = []; this.changeDetector = null; - this.elementRefs = ListWrapper.createFixedSize(elementCount); - for (var i=0; i { } export function main() { - var defaultPreBuiltObjects = new PreBuiltObjects(null, new DummyView(1), null); + var defaultPreBuiltObjects = new PreBuiltObjects(null, new DummyView(), new DummyElementRef(), null); // An injector with more than 10 bindings will switch to the dynamic strategy var dynamicBindings = []; for (var i = 0; i < 20; i++) { dynamicBindings.push(bind(i).toValue(i)); - } + } function createPei(parent, index, bindings, distance = 1, hasShadowRoot = false, dirVariableBindings = null) { var directiveBinding = ListWrapper.map(bindings, b => { @@ -754,9 +748,9 @@ export function main() { }); it("should instantiate directives that depend on pre built objects", () => { - var protoView = new AppProtoView(null, null, null, null); + var protoView = new AppProtoView(null, null, null, null, null); var bindings = ListWrapper.concat([NeedsProtoViewRef], extraBindings); - var inj = injector(bindings, null, false, new PreBuiltObjects(null, null, protoView)); + var inj = injector(bindings, null, false, new PreBuiltObjects(null, null, null, protoView)); expect(inj.get(NeedsProtoViewRef).protoViewRef).toEqual(new ProtoViewRef(protoView)); }); @@ -947,7 +941,7 @@ export function main() { describe("refs", () => { it("should inject ElementRef", () => { var inj = injector(ListWrapper.concat([NeedsElementRef], extraBindings)); - expect(inj.get(NeedsElementRef).elementRef).toBe(defaultPreBuiltObjects.view.elementRefs[0]); + expect(inj.get(NeedsElementRef).elementRef).toBe(defaultPreBuiltObjects.elementRef); }); it("should inject ChangeDetectorRef of the component's view into the component", () => { @@ -955,10 +949,10 @@ export function main() { var view = new DummyView(); var childView = new DummyView(); childView.changeDetector = cd; - view.componentChildViews = [childView]; + view.spy('getNestedView').andReturn(childView); var binding = DirectiveBinding.createFromType(ComponentNeedsChangeDetectorRef, new dirAnn.Component()); var inj = injector(ListWrapper.concat([binding], extraBindings), null, true, - new PreBuiltObjects(null, view, null)); + new PreBuiltObjects(null, view, new DummyElementRef(), null)); expect(inj.get(ComponentNeedsChangeDetectorRef).changeDetectorRef).toBe(cd.ref); }); @@ -969,7 +963,7 @@ export function main() { view.changeDetector =cd; var binding = DirectiveBinding.createFromType(DirectiveNeedsChangeDetectorRef, new dirAnn.Directive()); var inj = injector(ListWrapper.concat([binding], extraBindings), null, false, - new PreBuiltObjects(null, view, null)); + new PreBuiltObjects(null, view, new DummyElementRef(), null)); expect(inj.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef).toBe(cd.ref); }); @@ -980,9 +974,9 @@ export function main() { }); it("should inject ProtoViewRef", () => { - var protoView = new AppProtoView(null, null, null, null); + var protoView = new AppProtoView(null, null, null, null, null); var inj = injector(ListWrapper.concat([NeedsProtoViewRef], extraBindings), null, false, - new PreBuiltObjects(null, null, protoView)); + new PreBuiltObjects(null, null, null, protoView)); expect(inj.get(NeedsProtoViewRef).protoViewRef).toEqual(new ProtoViewRef(protoView)); }); @@ -1071,7 +1065,7 @@ export function main() { var inj = injector(ListWrapper.concat(dirs, extraBindings), null, false, preBuildObjects, null, dirVariableBindings); - expect(inj.get(NeedsQueryByVarBindings).query.first).toBe(defaultPreBuiltObjects.view.elementRefs[0]); + expect(inj.get(NeedsQueryByVarBindings).query.first).toBe(defaultPreBuiltObjects.elementRef); }); it('should contain directives on the same injector when querying by variable bindings' + @@ -1198,7 +1192,7 @@ export function main() { }); }); }); - }); + }); } class ContextWithHandler { diff --git a/modules/angular2/test/core/compiler/integration_spec.ts b/modules/angular2/test/core/compiler/integration_spec.ts index 52a8b599b8..2cb14f7cf9 100644 --- a/modules/angular2/test/core/compiler/integration_spec.ts +++ b/modules/angular2/test/core/compiler/integration_spec.ts @@ -15,7 +15,8 @@ import { xit, containsRegexp, stringifyElement, - TestComponentBuilder + TestComponentBuilder, + fakeAsync } from 'angular2/test_lib'; @@ -1369,8 +1370,8 @@ class SimpleImperativeViewComponent { done; constructor(self: ElementRef, viewManager: AppViewManager, renderer: DomRenderer) { - var shadowViewRef = viewManager.getComponentView(self); - renderer.setComponentViewRootNodes(shadowViewRef.render, [el('hello imp view')]); + var hostElement = renderer.getNativeElementSync(self); + DOM.appendChild(hostElement, el('hello imp view')); } } @@ -1870,7 +1871,7 @@ class SomeImperativeViewport { } if (value) { this.view = this.vc.create(this.protoView); - var nodes = this.renderer.getRootNodes(this.view.render); + var nodes = this.renderer.getRootNodes(this.view.renderFragment); for (var i = 0; i < nodes.length; i++) { DOM.appendChild(this.anchor, nodes[i]); } diff --git a/modules/angular2/test/core/compiler/projection_integration_spec.ts b/modules/angular2/test/core/compiler/projection_integration_spec.ts new file mode 100644 index 0000000000..74fa39e259 --- /dev/null +++ b/modules/angular2/test/core/compiler/projection_integration_spec.ts @@ -0,0 +1,492 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + IS_DARTIUM, + beforeEachBindings, + it, + xit, + containsRegexp, + stringifyElement, + TestComponentBuilder, + RootTestComponent, + fakeAsync, + tick, + By +} from 'angular2/test_lib'; + +import {DOM} from 'angular2/src/dom/dom_adapter'; + +import * as viewAnn from 'angular2/src/core/annotations_impl/view'; + +import { + Component, + Directive, + View, + forwardRef, + ViewContainerRef, + ProtoViewRef, + ElementRef, + bind +} from 'angular2/angular2'; +import {ShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/render'; + +export function main() { + describe('projection', () => { + it('should support simple components', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
A
' + + '
', + directives: [Simple] + })) + .createAsync(MainComp) + .then((main) => { + expect(main.nativeElement).toHaveText('SIMPLE(A)'); + async.done(); + }); + })); + + it('should support simple components with text interpolation as direct children', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '{{\'START(\'}}' + + '{{text}}' + + '{{\')END\'}}', + directives: [Simple] + })) + .createAsync(MainComp) + .then((main) => { + + main.componentInstance.text = 'A'; + main.detectChanges(); + expect(main.nativeElement).toHaveText('START(SIMPLE(A))END'); + async.done(); + }); + })); + + it('should not show the light dom even if there is no content tag', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, + new viewAnn.View({template: 'A', directives: [Empty]})) + .createAsync(MainComp) + .then((main) => { + + expect(main.nativeElement).toHaveText(''); + async.done(); + }); + })); + + it('should support multiple content tags', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
B
' + + '
C
' + + '
A
' + + '
', + directives: [MultipleContentTagsComponent] + })) + .createAsync(MainComp) + .then((main) => { + + expect(main.nativeElement).toHaveText('(A, BC)'); + async.done(); + }); + })); + + it('should redistribute only direct children', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
B
A
' + + '
C
' + + '
', + directives: [MultipleContentTagsComponent] + })) + .createAsync(MainComp) + .then((main) => { + + expect(main.nativeElement).toHaveText('(, BAC)'); + async.done(); + }); + })); + + it("should redistribute direct child viewcontainers when the light dom changes", + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '' + + '
B
' + + '
', + directives: [MultipleContentTagsComponent, ManualViewportDirective] + })) + .createAsync(MainComp) + .then((main) => { + + var viewportDirectives = main.queryAll(By.directive(ManualViewportDirective)) + .map(de => de.inject(ManualViewportDirective)); + + expect(main.nativeElement).toHaveText('(, B)'); + viewportDirectives.forEach(d => d.show()); + expect(main.nativeElement).toHaveText('(A1, B)'); + + viewportDirectives.forEach(d => d.hide()); + + expect(main.nativeElement).toHaveText('(, B)'); + async.done(); + }); + })); + + it("should support nested components", + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
A
' + + '
B
' + + '
', + directives: [OuterWithIndirectNestedComponent] + })) + .createAsync(MainComp) + .then((main) => { + + expect(main.nativeElement).toHaveText('OUTER(SIMPLE(AB))'); + async.done(); + }); + })); + + it("should support nesting with content being direct child of a nested component", + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '' + + '
B
' + + '
C
' + + '
', + directives: [OuterComponent, ManualViewportDirective], + })) + .createAsync(MainComp) + .then((main) => { + + var viewportDirective = main.query(By.directive(ManualViewportDirective)) + .inject(ManualViewportDirective); + + expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(,BC)))'); + viewportDirective.show(); + + expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(A,BC)))'); + async.done(); + }); + })); + + it('should redistribute when the shadow dom changes', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
A
' + + '
B
' + + '
C
' + + '
', + directives: [ConditionalContentComponent] + })) + .createAsync(MainComp) + .then((main) => { + + var viewportDirective = main.query(By.directive(ManualViewportDirective)) + .inject(ManualViewportDirective); + + expect(main.nativeElement).toHaveText('(, BC)'); + + viewportDirective.show(); + expect(main.nativeElement).toHaveText('(A, BC)'); + + viewportDirective.hide(); + + expect(main.nativeElement).toHaveText('(, BC)'); + async.done(); + }); + })); + + // GH-2095 - https://github.com/angular/angular/issues/2095 + // important as we are removing the ng-content element during compilation, + // which could skrew up text node indices. + it('should support text nodes after content tags', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb, async) => { + + tcb.overrideView( + MainComp, + new viewAnn.View( + {template: '', directives: [Simple]})) + .overrideTemplate(Simple, '

P,

{{stringProp}}') + .createAsync(MainComp) + .then((main) => { + + main.detectChanges(); + + expect(main.nativeElement).toHaveText('P,text'); + async.done(); + }); + + })); + + // important as we are moving style tags around during compilation, + // which could skrew up text node indices. + it('should support text nodes after style tags', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb, async) => { + + tcb.overrideView( + MainComp, + new viewAnn.View( + {template: '', directives: [Simple]})) + .overrideTemplate(Simple, '

P,

{{stringProp}}') + .createAsync(MainComp) + .then((main) => { + + main.detectChanges(); + expect(main.nativeElement).toHaveText('P,text'); + async.done(); + }); + })); + + it('should support moving non projected light dom around', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + ' ' + + '' + + 'START(
)END', + directives: [Empty, ProjectDirective, ManualViewportDirective], + })) + .createAsync(MainComp) + .then((main) => { + + var sourceDirective: ManualViewportDirective = + main.query(By.directive(ManualViewportDirective)) + .inject(ManualViewportDirective); + var projectDirective: ProjectDirective = + main.query(By.directive(ProjectDirective)).inject(ProjectDirective); + expect(main.nativeElement).toHaveText('START()END'); + + projectDirective.show(sourceDirective.protoViewRef, sourceDirective.elementRef); + expect(main.nativeElement).toHaveText('START(A)END'); + async.done(); + }); + })); + + it('should support moving projected light dom around', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + 'START(
)END', + directives: [Simple, ProjectDirective, ManualViewportDirective], + })) + .createAsync(MainComp) + .then((main) => { + + var sourceDirective: ManualViewportDirective = + main.query(By.directive(ManualViewportDirective)) + .inject(ManualViewportDirective); + var projectDirective: ProjectDirective = + main.query(By.directive(ProjectDirective)).inject(ProjectDirective); + expect(main.nativeElement).toHaveText('SIMPLE()START()END'); + + projectDirective.show(sourceDirective.protoViewRef, sourceDirective.elementRef); + expect(main.nativeElement).toHaveText('SIMPLE()START(A)END'); + async.done(); + }); + })); + + it('should support moving ng-content around', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
A
' + + '
B
' + + '
' + + 'START(
)END', + directives: + [ConditionalContentComponent, ProjectDirective, ManualViewportDirective] + })) + .createAsync(MainComp) + .then((main) => { + + var sourceDirective: ManualViewportDirective = + main.query(By.directive(ManualViewportDirective)) + .inject(ManualViewportDirective); + var projectDirective: ProjectDirective = + main.query(By.directive(ProjectDirective)).inject(ProjectDirective); + expect(main.nativeElement).toHaveText('(, B)START()END'); + + projectDirective.show(sourceDirective.protoViewRef, sourceDirective.elementRef); + expect(main.nativeElement).toHaveText('(, B)START(A)END'); + + // Stamping ng-content multiple times should not produce the content multiple + // times... + projectDirective.show(sourceDirective.protoViewRef, sourceDirective.elementRef); + expect(main.nativeElement).toHaveText('(, B)START(A)END'); + async.done(); + }); + })); + + + // Note: This does not use a ng-content element, but + // is still important as we are merging proto views independent of + // the presence of ng-content elements! + it('should still allow to implement a recursive trees', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, + new viewAnn.View({template: '', directives: [Tree]})) + .createAsync(MainComp) + .then((main) => { + + main.detectChanges(); + var manualDirective: ManualViewportDirective = + main.query(By.directive(ManualViewportDirective)) + .inject(ManualViewportDirective); + expect(main.nativeElement).toHaveText('TREE(0:)'); + manualDirective.show(); + main.detectChanges(); + expect(main.nativeElement).toHaveText('TREE(0:TREE(1:))'); + async.done(); + }); + })); + + if (DOM.supportsNativeShadowDOM()) { + describe('native shadow dom support', () => { + beforeEachBindings( + () => { return [bind(ShadowDomStrategy).toValue(new NativeShadowDomStrategy())]; }); + + it('should support native content projection', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MainComp, new viewAnn.View({ + template: '' + + '
A
' + + '
', + directives: [SimpleNative] + })) + .createAsync(MainComp) + .then((main) => { + + expect(main.nativeElement).toHaveText('SIMPLE(A)'); + async.done(); + }); + })); + }); + } + + }); +} + +@Component({selector: 'main'}) +@View({template: '', directives: []}) +class MainComp { + text: string = ''; +} + +@Component({selector: 'simple', properties: ['stringProp']}) +@View({template: 'SIMPLE()', directives: []}) +class Simple { + stringProp: string = ''; +} + +@Component({selector: 'simple-native'}) +@View({template: 'SIMPLE()', directives: []}) +class SimpleNative { +} + +@Component({selector: 'empty'}) +@View({template: '', directives: []}) +class Empty { +} + +@Component({selector: 'multiple-content-tags'}) +@View({ + template: '(, )', + directives: [] +}) +class MultipleContentTagsComponent { +} + +@Directive({selector: '[manual]'}) +class ManualViewportDirective { + constructor(public vc: ViewContainerRef, public protoViewRef: ProtoViewRef, + public elementRef: ElementRef) {} + show() { this.vc.create(this.protoViewRef, 0); } + hide() { this.vc.clear(); } +} + +@Directive({selector: '[project]'}) +class ProjectDirective { + constructor(public vc: ViewContainerRef) {} + show(protoViewRef: ProtoViewRef, context: ElementRef) { + this.vc.create(protoViewRef, 0, context); + } + hide() { this.vc.clear(); } +} + +@Component({selector: 'outer-with-indirect-nested'}) +@View({ + template: 'OUTER(
)', + directives: [Simple] +}) +class OuterWithIndirectNestedComponent { +} + +@Component({selector: 'outer'}) +@View({ + template: 'OUTER()', + directives: [forwardRef(() => InnerComponent)] +}) +class OuterComponent { +} + +@Component({selector: 'inner'}) +@View({ + template: 'INNER()', + directives: [forwardRef(() => InnerInnerComponent)] +}) +class InnerComponent { +} + +@Component({selector: 'innerinner'}) +@View({ + template: 'INNERINNER(,)', + directives: [] +}) +class InnerInnerComponent { +} + +@Component({selector: 'conditional-content'}) +@View({ + template: + '
(
, )
', + directives: [ManualViewportDirective] +}) +class ConditionalContentComponent { +} + +@Component({selector: 'tab'}) +@View({ + template: '
TAB()
', + directives: [ManualViewportDirective] +}) +class Tab { +} + +@Component({selector: 'tree', properties: ['depth']}) +@View({ + template: 'TREE({{depth}}:)', + directives: [ManualViewportDirective, Tree] +}) +class Tree { + depth = 0; +} \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts index ea5141c03f..6d2cdef5e6 100644 --- a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts +++ b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts @@ -35,7 +35,7 @@ export function main() { describe('ProtoViewFactory', () => { var changeDetection; - var protoViewFactory; + var protoViewFactory: ProtoViewFactory; var directiveResolver; beforeEach(() => { @@ -63,10 +63,13 @@ export function main() { describe('createAppProtoViews', () => { it('should create an AppProtoView for the root render proto view', () => { - var renderPv = createRenderProtoView(); - var pvs = protoViewFactory.createAppProtoViews(bindDirective(MainComponent), renderPv, []); - expect(pvs.length).toBe(1); - expect(pvs[0].render).toBe(renderPv.render); + var varBindings = new Map(); + varBindings.set('a', 'b'); + var renderPv = createRenderProtoView([], null, varBindings); + var appPv = + protoViewFactory.createAppProtoViews(bindDirective(MainComponent), renderPv, []); + expect(appPv.variableBindings.get('a')).toEqual('b'); + expect(appPv).toBeTruthy(); }); }); @@ -159,15 +162,23 @@ function directiveBinding({metadata}: {metadata?: any} = {}) { return new DirectiveBinding(Key.get("dummy"), null, [], [], [], metadata); } -function createRenderProtoView(elementBinders = null, type: renderApi.ViewType = null) { +function createRenderProtoView(elementBinders = null, type: renderApi.ViewType = null, + variableBindings = null) { if (isBlank(type)) { type = renderApi.ViewType.COMPONENT; } if (isBlank(elementBinders)) { elementBinders = []; } - return new renderApi.ProtoViewDto( - {elementBinders: elementBinders, type: type, variableBindings: new Map()}); + if (isBlank(variableBindings)) { + variableBindings = new Map(); + } + return new renderApi.ProtoViewDto({ + elementBinders: elementBinders, + type: type, + variableBindings: variableBindings, + textBindings: [] + }); } function createRenderComponentElementBinder(directiveIndex) { diff --git a/modules/angular2/test/core/compiler/query_integration_spec.ts b/modules/angular2/test/core/compiler/query_integration_spec.ts index 14ef1c5dfc..e5049a4482 100644 --- a/modules/angular2/test/core/compiler/query_integration_spec.ts +++ b/modules/angular2/test/core/compiler/query_integration_spec.ts @@ -398,7 +398,7 @@ class NeedsQueryDesc { } @Component({selector: 'needs-query-by-var-binding'}) -@View({directives: [], template: ''}) +@View({directives: [], template: ''}) @Injectable() class NeedsQueryByLabel { query: QueryList; @@ -408,7 +408,7 @@ class NeedsQueryByLabel { } @Component({selector: 'needs-query-by-var-bindings'}) -@View({directives: [], template: ''}) +@View({directives: [], template: ''}) @Injectable() class NeedsQueryByTwoLabels { query: QueryList; diff --git a/modules/angular2/test/core/compiler/view_container_ref_spec.ts b/modules/angular2/test/core/compiler/view_container_ref_spec.ts index 4ac6c95fb0..4438308d7d 100644 --- a/modules/angular2/test/core/compiler/view_container_ref_spec.ts +++ b/modules/angular2/test/core/compiler/view_container_ref_spec.ts @@ -18,10 +18,11 @@ import { import {IMPLEMENTS} from 'angular2/src/facade/lang'; -import {AppView, AppProtoView, AppViewContainer} from 'angular2/src/core/compiler/view'; +import {AppView, AppViewContainer} from 'angular2/src/core/compiler/view'; import {ViewContainerRef} from 'angular2/src/core/compiler/view_container_ref'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; -import {ElementBinder} from 'angular2/src/core/compiler/element_binder'; +import {ElementRef} from 'angular2/src/core/compiler/element_ref'; +import {ViewRef} from 'angular2/src/core/compiler/view_ref'; export function main() { // TODO(tbosch): add missing tests @@ -31,34 +32,26 @@ export function main() { var view; var viewManager; - function createProtoView() { - var pv = new AppProtoView(null, null, null, null); - pv.elementBinders = [new ElementBinder(0, null, 0, null, null)]; - return pv; - } - - function createView() { return new AppView(null, createProtoView(), new Map()); } - function createViewContainer() { return new ViewContainerRef(viewManager, location); } beforeEach(() => { viewManager = new AppViewManagerSpy(); - view = createView(); - view.viewContainers = [null]; - location = view.elementRefs[0]; + view = new AppViewSpy(); + location = new ElementRef(new ViewRef(view), 0, 0, null); }); describe('length', () => { - it('should return a 0 length if there is no underlying ViewContainerRef', () => { + it('should return a 0 length if there is no underlying AppViewContainer', () => { var vc = createViewContainer(); expect(vc.length).toBe(0); }); - it('should return the size of the underlying ViewContainerRef', () => { + it('should return the size of the underlying AppViewContainer', () => { var vc = createViewContainer(); - view.viewContainers = [new AppViewContainer()]; - view.viewContainers[0].views = [createView()]; + var appVc = new AppViewContainer(); + view.viewContainers = [appVc]; + appVc.views = [new AppViewSpy()]; expect(vc.length).toBe(1); }); @@ -69,6 +62,14 @@ export function main() { }); } +@proxy +@IMPLEMENTS(AppView) +class AppViewSpy extends SpyObject { + viewContainers: AppViewContainer[] = [null]; + constructor() { super(AppView); } + noSuchMethod(m) { return super.noSuchMethod(m) } +} + @proxy @IMPLEMENTS(AppViewManager) class AppViewManagerSpy extends SpyObject { diff --git a/modules/angular2/test/core/compiler/view_manager_spec.ts b/modules/angular2/test/core/compiler/view_manager_spec.ts index 014d531496..e2a0ebd417 100644 --- a/modules/angular2/test/core/compiler/view_manager_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_spec.ts @@ -16,240 +16,155 @@ import { proxy } from 'angular2/test_lib'; import {Injector, bind} from 'angular2/di'; -import {IMPLEMENTS, isBlank, isPresent} from 'angular2/src/facade/lang'; -import {MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; +import {IMPLEMENTS} from 'angular2/src/facade/lang'; -import {AppProtoView, AppView, AppViewContainer} from 'angular2/src/core/compiler/view'; +import { + AppProtoView, + AppView, + AppViewContainer, + AppProtoViewMergeMapping +} from 'angular2/src/core/compiler/view'; import {ProtoViewRef, ViewRef, internalView} from 'angular2/src/core/compiler/view_ref'; -import {Renderer, RenderViewRef, RenderProtoViewRef} from 'angular2/src/render/api'; -import {ElementBinder} from 'angular2/src/core/compiler/element_binder'; -import {DirectiveBinding, ElementInjector} from 'angular2/src/core/compiler/element_injector'; -import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; -import {Component} from 'angular2/annotations'; +import {ElementRef} from 'angular2/src/core/compiler/element_ref'; +import { + Renderer, + RenderViewRef, + RenderProtoViewRef, + RenderFragmentRef, + ViewType, + RenderProtoViewMergeMapping, + RenderViewWithFragments +} from 'angular2/src/render/api'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils'; import {AppViewListener} from 'angular2/src/core/compiler/view_listener'; import {AppViewPool} from 'angular2/src/core/compiler/view_pool'; +import { + createHostPv, + createComponentPv, + createEmbeddedPv, + createEmptyElBinder, + createNestedElBinder, + createProtoElInjector +} from './view_manager_utils_spec'; + export function main() { // TODO(tbosch): add missing tests describe('AppViewManager', () => { var renderer; - var utils; + var utils: AppViewManagerUtils; var viewListener; var viewPool; - var manager; - var directiveResolver; - var createdViews: any[]; - var createdRenderViews: any[]; + var manager: AppViewManager; + var createdRenderViews: RenderViewWithFragments[]; function wrapPv(protoView: AppProtoView): ProtoViewRef { return new ProtoViewRef(protoView); } function wrapView(view: AppView): ViewRef { return new ViewRef(view); } - function elementRef(parentView, boundElementIndex) { - return parentView.elementRefs[boundElementIndex]; - } - - function createDirectiveBinding(type) { - var annotation = directiveResolver.resolve(type); - return DirectiveBinding.createFromType(type, annotation); - } - - function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null); } - - function createComponentElBinder(nestedProtoView = null) { - var binding = createDirectiveBinding(SomeComponent); - var binder = new ElementBinder(0, null, 0, null, binding); - binder.nestedProtoView = nestedProtoView; - return binder; - } - - function createProtoView(binders = null) { - if (isBlank(binders)) { - binders = []; - } - var staticChildComponentCount = 0; - for (var i = 0; i < binders.length; i++) { - if (binders[i].hasStaticComponent()) { - staticChildComponentCount++; - } - } - var res = new AppProtoView(new MockProtoViewRef(staticChildComponentCount), null, null, null); - res.elementBinders = binders; - return res; - } - - function createElementInjector() { - return SpyObject.stub(new SpyElementInjector(), - { - 'isExportingComponent': false, - 'isExportingElement': false, - 'getEventEmitterAccessors': [], - 'getComponent': null - }, - {}); - } - - function createView(pv = null, renderViewRef = null) { - if (isBlank(pv)) { - pv = createProtoView(); - } - if (isBlank(renderViewRef)) { - renderViewRef = new RenderViewRef(); - } - var view = new AppView(renderer, pv, new Map()); - view.render = renderViewRef; - var elementInjectors = ListWrapper.createFixedSize(pv.elementBinders.length); - for (var i = 0; i < pv.elementBinders.length; i++) { - elementInjectors[i] = createElementInjector(); - } - view.init(null, elementInjectors, [], ListWrapper.createFixedSize(pv.elementBinders.length), - ListWrapper.createFixedSize(pv.elementBinders.length)); - return view; + function resetSpies() { + viewListener.spy('viewCreated').reset(); + viewListener.spy('viewDestroyed').reset(); + renderer.spy('createView').reset(); + renderer.spy('destroyView').reset(); + renderer.spy('createRootHostView').reset(); + renderer.spy('setEventDispatcher').reset(); + renderer.spy('hydrateView').reset(); + renderer.spy('dehydrateView').reset(); + viewPool.spy('returnView').reset(); } beforeEach(() => { - directiveResolver = new DirectiveResolver(); renderer = new SpyRenderer(); - utils = new SpyAppViewManagerUtils(); + utils = new AppViewManagerUtils(); viewListener = new SpyAppViewListener(); viewPool = new SpyAppViewPool(); manager = new AppViewManager(viewPool, viewListener, utils, renderer); - createdViews = []; createdRenderViews = []; - utils.spy('createView') - .andCallFake((proto, renderViewRef, _a, _b) => { - var view = createView(proto, renderViewRef); - createdViews.push(view); - return view; - }); - utils.spy('attachComponentView') - .andCallFake((hostView, elementIndex, childView) => { - hostView.componentChildViews[elementIndex] = childView; - }); - utils.spy('attachViewInContainer') - .andCallFake((parentView, elementIndex, _a, _b, atIndex, childView) => { - var viewContainer = parentView.viewContainers[elementIndex]; - if (isBlank(viewContainer)) { - viewContainer = new AppViewContainer(); - parentView.viewContainers[elementIndex] = viewContainer; - } - ListWrapper.insert(viewContainer.views, atIndex, childView); - }); renderer.spy('createRootHostView') - .andCallFake((_b, _c) => { - var rv = new RenderViewRef(); + .andCallFake((_a, renderFragmentCount, _b) => { + var fragments = []; + for (var i = 0; i < renderFragmentCount; i++) { + fragments.push(new RenderFragmentRef()); + } + var rv = new RenderViewWithFragments(new RenderViewRef(), fragments); createdRenderViews.push(rv); return rv; }); renderer.spy('createView') - .andCallFake((_a) => { - var rv = new RenderViewRef(); + .andCallFake((_a, renderFragmentCount) => { + var fragments = []; + for (var i = 0; i < renderFragmentCount; i++) { + fragments.push(new RenderFragmentRef()); + } + var rv = new RenderViewWithFragments(new RenderViewRef(), fragments); createdRenderViews.push(rv); return rv; }); viewPool.spy('returnView').andReturn(true); }); - describe('static child components', () => { - - describe('recursively create when not cached', () => { - var rootProtoView, hostProtoView, componentProtoView, hostView, componentView; - beforeEach(() => { - componentProtoView = createProtoView(); - hostProtoView = createProtoView([createComponentElBinder(componentProtoView)]); - rootProtoView = createProtoView([createComponentElBinder(hostProtoView)]); - manager.createRootHostView(wrapPv(rootProtoView), null, null); - hostView = createdViews[1]; - componentView = createdViews[2]; - }); - - it('should create the view', () => { - expect(hostView.proto).toBe(hostProtoView); - expect(componentView.proto).toBe(componentProtoView); - }); - - it('should hydrate the view', () => { - expect(utils.spy('hydrateComponentView')).toHaveBeenCalledWith(hostView, 0); - expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(componentView.render); - }); - - it('should set the render view', - () => {expect(componentView.render).toBe(createdRenderViews[2])}); - - it('should set the event dispatcher', () => { - expect(renderer.spy('setEventDispatcher')) - .toHaveBeenCalledWith(componentView.render, componentView); - }); - }); - - describe('recursively hydrate when getting from from the cache', - () => { - // TODO(tbosch): implement this - }); - - describe('recursively dehydrate', () => { - // TODO(tbosch): implement this - }); - - }); - describe('createRootHostView', () => { - var hostProtoView; - beforeEach(() => { hostProtoView = createProtoView([createComponentElBinder(null)]); }); + var hostProtoView: AppProtoView; + beforeEach( + () => { hostProtoView = createHostPv([createNestedElBinder(createComponentPv())]); }); it('should create the view', () => { - expect(internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null))) - .toBe(createdViews[0]); - expect(createdViews[0].proto).toBe(hostProtoView); - expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); + var rootView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + expect(rootView.proto).toBe(hostProtoView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(rootView); }); it('should hydrate the view', () => { var injector = Injector.resolveAndCreate([]); - manager.createRootHostView(wrapPv(hostProtoView), null, injector); - expect(utils.spy('hydrateRootHostView')).toHaveBeenCalledWith(createdViews[0], injector); - expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); + var rootView = + internalView(manager.createRootHostView(wrapPv(hostProtoView), null, injector)); + expect(rootView.hydrated()).toBe(true); + expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(rootView.render); }); it('should create and set the render view using the component selector', () => { - manager.createRootHostView(wrapPv(hostProtoView), null, null) - expect(renderer.spy('createRootHostView')) - .toHaveBeenCalledWith(hostProtoView.render, 'someComponent'); - expect(createdViews[0].render).toBe(createdRenderViews[0]); + var rootView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + expect(renderer.spy('createRootHostView')) + .toHaveBeenCalledWith(hostProtoView.mergeMapping.renderProtoViewRef, + hostProtoView.mergeMapping.renderFragmentCount, 'someComponent'); + expect(rootView.render).toBe(createdRenderViews[0].viewRef); + expect(rootView.renderFragment).toBe(createdRenderViews[0].fragmentRefs[0]); }); it('should allow to override the selector', () => { var selector = 'someOtherSelector'; - manager.createRootHostView(wrapPv(hostProtoView), selector, null) - expect(renderer.spy('createRootHostView')) - .toHaveBeenCalledWith(hostProtoView.render, selector); + internalView(manager.createRootHostView(wrapPv(hostProtoView), selector, null)); + expect(renderer.spy('createRootHostView')) + .toHaveBeenCalledWith(hostProtoView.mergeMapping.renderProtoViewRef, + hostProtoView.mergeMapping.renderFragmentCount, selector); }); it('should set the event dispatcher', () => { - manager.createRootHostView(wrapPv(hostProtoView), null, null); - var cmpView = createdViews[0]; - expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView); + var rootView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(rootView.render, rootView); }); }); describe('destroyRootHostView', () => { - var hostProtoView, hostView, hostRenderViewRef; + var hostProtoView: AppProtoView; + var hostView: AppView; + var hostRenderViewRef: RenderViewRef; beforeEach(() => { - hostProtoView = createProtoView([createComponentElBinder(null)]); + hostProtoView = createHostPv([createNestedElBinder(createComponentPv())]); hostView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); hostRenderViewRef = hostView.render; }); it('should dehydrate', () => { manager.destroyRootHostView(wrapView(hostView)); - expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(hostView); + expect(hostView.hydrated()).toBe(false); expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(hostView.render); }); @@ -269,60 +184,120 @@ export function main() { describe('createViewInContainer', () => { describe('basic functionality', () => { - var parentView, childProtoView; + var hostView: AppView; + var childProtoView: AppProtoView; + var vcRef: ElementRef; beforeEach(() => { - parentView = createView(createProtoView([createEmptyElBinder()])); - childProtoView = createProtoView(); + childProtoView = createEmbeddedPv(); + var hostProtoView = createHostPv( + [createNestedElBinder(createComponentPv([createNestedElBinder(childProtoView)]))]); + hostView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + vcRef = hostView.elementRefs[1]; + resetSpies(); }); - it('should create a ViewContainerRef if not yet existing', () => { - manager.createViewInContainer(elementRef(parentView, 0), 0, wrapPv(childProtoView), null); - expect(parentView.viewContainers[0]).toBeTruthy(); + describe('create the first view', () => { + + it('should create an AppViewContainer if not yet existing', () => { + manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null); + expect(hostView.viewContainers[1]).toBeTruthy(); + }); + + it('should use an existing nested view', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + expect(childView.proto).toBe(childProtoView); + expect(childView).toBe(hostView.views[2]); + expect(viewListener.spy('viewCreated')).not.toHaveBeenCalled(); + expect(renderer.spy('createView')).not.toHaveBeenCalled(); + }); + + it('should attach the fragment', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + expect(childView.proto).toBe(childProtoView); + expect(hostView.viewContainers[1].views.length).toBe(1); + expect(hostView.viewContainers[1].views[0]).toBe(childView); + expect(renderer.spy('attachFragmentAfterElement')) + .toHaveBeenCalledWith(vcRef, childView.renderFragment); + }); + + it('should hydrate the view but not the render view', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + expect(childView.hydrated()).toBe(true); + expect(renderer.spy('hydrateView')).not.toHaveBeenCalled(); + }); + + it('should not set the EventDispatcher', () => { + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + expect(renderer.spy('setEventDispatcher')).not.toHaveBeenCalled(); + }); + }); - it('should create the view', () => { - expect(internalView(manager.createViewInContainer(elementRef(parentView, 0), 0, - wrapPv(childProtoView), null))) - .toBe(createdViews[0]); - expect(createdViews[0].proto).toBe(childProtoView); - expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); + describe('create the second view', () => { + var firstChildView; + beforeEach(() => { + firstChildView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + resetSpies(); + }); + + it('should create a new view', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 1, wrapPv(childProtoView), null)); + expect(childView.proto).toBe(childProtoView); + expect(childView).not.toBe(firstChildView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(childView); + expect(renderer.spy('createView')) + .toHaveBeenCalledWith(childProtoView.mergeMapping.renderProtoViewRef, + childProtoView.mergeMapping.renderFragmentCount); + expect(childView.render).toBe(createdRenderViews[1].viewRef); + expect(childView.renderFragment).toBe(createdRenderViews[1].fragmentRefs[0]); + }); + + it('should attach the fragment', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 1, wrapPv(childProtoView), null)); + expect(childView.proto).toBe(childProtoView); + expect(hostView.viewContainers[1].views[1]).toBe(childView); + expect(renderer.spy('attachFragmentAfterFragment')) + .toHaveBeenCalledWith(firstChildView.renderFragment, childView.renderFragment); + }); + + it('should hydrate the view', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 1, wrapPv(childProtoView), null)); + expect(childView.hydrated()).toBe(true); + expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(childView.render); + }); + + it('should set the EventDispatcher', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 1, wrapPv(childProtoView), null)); + expect(renderer.spy('setEventDispatcher')) + .toHaveBeenCalledWith(childView.render, childView); + }); + }); - it('should attach the view', () => { - var contextView = - createView(createProtoView([createEmptyElBinder(), createEmptyElBinder()])); - var elRef = elementRef(parentView, 0); - manager.createViewInContainer(elRef, 0, wrapPv(childProtoView), - elementRef(contextView, 1), null); - expect(utils.spy('attachViewInContainer')) - .toHaveBeenCalledWith(parentView, 0, contextView, 1, 0, createdViews[0]); - expect(renderer.spy('attachViewInContainer')) - .toHaveBeenCalledWith(elRef, 0, createdViews[0].render); - }); + describe('create another view when the first view has been returned', () => { + beforeEach(() => { + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + manager.destroyViewInContainer(vcRef, 0); + resetSpies(); + }); - it('should hydrate the view', () => { - var contextView = - createView(createProtoView([createEmptyElBinder(), createEmptyElBinder()])); - manager.createViewInContainer(elementRef(parentView, 0), 0, wrapPv(childProtoView), - elementRef(contextView, 1), []); - expect(utils.spy('hydrateViewInContainer')) - .toHaveBeenCalledWith(parentView, 0, contextView, 1, 0, []); - expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); - }); + it('should use an existing nested view', () => { + var childView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + expect(childView.proto).toBe(childProtoView); + expect(childView).toBe(hostView.views[2]); + expect(viewListener.spy('viewCreated')).not.toHaveBeenCalled(); + expect(renderer.spy('createView')).not.toHaveBeenCalled(); + }); - it('should create and set the render view', () => { - manager.createViewInContainer(elementRef(parentView, 0), 0, wrapPv(childProtoView), null, - null); - expect(renderer.spy('createView')).toHaveBeenCalledWith(childProtoView.render); - expect(createdViews[0].render).toBe(createdRenderViews[0]); - }); - - it('should set the event dispatcher', () => { - manager.createViewInContainer(elementRef(parentView, 0), 0, wrapPv(childProtoView), null, - null); - var childView = createdViews[0]; - expect(renderer.spy('setEventDispatcher')) - .toHaveBeenCalledWith(childView.render, childView); }); }); @@ -331,61 +306,159 @@ export function main() { describe('destroyViewInContainer', () => { describe('basic functionality', () => { - var parentView, childProtoView, childView; + var hostView: AppView; + var childProtoView: AppProtoView; + var vcRef: ElementRef; + var firstChildView: AppView; beforeEach(() => { - parentView = createView(createProtoView([createEmptyElBinder()])); - childProtoView = createProtoView(); - childView = internalView(manager.createViewInContainer(elementRef(parentView, 0), 0, - wrapPv(childProtoView), null)); + childProtoView = createEmbeddedPv(); + var hostProtoView = createHostPv( + [createNestedElBinder(createComponentPv([createNestedElBinder(childProtoView)]))]); + hostView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + vcRef = hostView.elementRefs[1]; + firstChildView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + resetSpies(); }); - it('should dehydrate', () => { - manager.destroyViewInContainer(elementRef(parentView, 0), 0); - expect(utils.spy('dehydrateView')) - .toHaveBeenCalledWith(parentView.viewContainers[0].views[0]); - expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render); + describe('destroy the first view', () => { + it('should dehydrate the app view but not the render view', () => { + manager.destroyViewInContainer(vcRef, 0); + expect(firstChildView.hydrated()).toBe(false); + expect(renderer.spy('dehydrateView')).not.toHaveBeenCalled(); + }); + + it('should detach', () => { + manager.destroyViewInContainer(vcRef, 0); + expect(hostView.viewContainers[1].views).toEqual([]); + expect(renderer.spy('detachFragment')) + .toHaveBeenCalledWith(firstChildView.renderFragment); + }); + + it('should not return the view to the pool', () => { + manager.destroyViewInContainer(vcRef, 0); + expect(viewPool.spy('returnView')).not.toHaveBeenCalled(); + }); + }); - it('should detach', () => { - var elRef = elementRef(parentView, 0); - manager.destroyViewInContainer(elRef, 0); - expect(utils.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0); - expect(renderer.spy('detachViewInContainer')) - .toHaveBeenCalledWith(elRef, 0, childView.render); - }); + describe('destroy another view', () => { + var secondChildView; + beforeEach(() => { + secondChildView = + internalView(manager.createViewInContainer(vcRef, 1, wrapPv(childProtoView), null)); + resetSpies(); + }); + + it('should dehydrate', () => { + manager.destroyViewInContainer(vcRef, 1); + expect(secondChildView.hydrated()).toBe(false); + expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(secondChildView.render); + }); + + it('should detach', () => { + manager.destroyViewInContainer(vcRef, 1); + expect(hostView.viewContainers[1].views[0]).toBe(firstChildView); + expect(renderer.spy('detachFragment')) + .toHaveBeenCalledWith(secondChildView.renderFragment); + }); + + it('should return the view to the pool', () => { + manager.destroyViewInContainer(vcRef, 1); + expect(viewPool.spy('returnView')).toHaveBeenCalledWith(secondChildView); + }); - it('should return the view to the pool', () => { - manager.destroyViewInContainer(elementRef(parentView, 0), 0); - expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView); }); }); describe('recursively destroy views in ViewContainers', () => { - var parentView, childProtoView, childView; - beforeEach(() => { - parentView = createView(createProtoView([createEmptyElBinder()])); - childProtoView = createProtoView(); - childView = internalView(manager.createViewInContainer(elementRef(parentView, 0), 0, - wrapPv(childProtoView), null)); + + describe('destroy child views when a component is destroyed', () => { + var hostView: AppView; + var childProtoView: AppProtoView; + var vcRef: ElementRef; + var firstChildView: AppView; + var secondChildView: AppView; + beforeEach(() => { + childProtoView = createEmbeddedPv(); + var hostProtoView = createHostPv( + [createNestedElBinder(createComponentPv([createNestedElBinder(childProtoView)]))]); + hostView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + vcRef = hostView.elementRefs[1]; + firstChildView = + internalView(manager.createViewInContainer(vcRef, 0, wrapPv(childProtoView), null)); + secondChildView = + internalView(manager.createViewInContainer(vcRef, 1, wrapPv(childProtoView), null)); + resetSpies(); + }); + + it('should dehydrate', () => { + manager.destroyRootHostView(wrapView(hostView)); + expect(firstChildView.hydrated()).toBe(false); + expect(secondChildView.hydrated()).toBe(false); + expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(hostView.render); + expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(secondChildView.render); + }); + + it('should detach', () => { + manager.destroyRootHostView(wrapView(hostView)); + expect(hostView.viewContainers[1].views).toEqual([]); + expect(renderer.spy('detachFragment')) + .toHaveBeenCalledWith(firstChildView.renderFragment); + expect(renderer.spy('detachFragment')) + .toHaveBeenCalledWith(secondChildView.renderFragment); + }); + + it('should return the view to the pool', () => { + manager.destroyRootHostView(wrapView(hostView)); + expect(viewPool.spy('returnView')).not.toHaveBeenCalledWith(firstChildView); + expect(viewPool.spy('returnView')).toHaveBeenCalledWith(secondChildView); + }); + }); - it('should dehydrate', () => { - manager.destroyRootHostView(wrapView(parentView)); - expect(utils.spy('dehydrateView')) - .toHaveBeenCalledWith(parentView.viewContainers[0].views[0]); - expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render); - }); + describe('destroy child views over multiple levels', () => { + var hostView: AppView; + var childProtoView: AppProtoView; + var nestedChildProtoView: AppProtoView; + var vcRef: ElementRef; + var nestedVcRefs: ElementRef[]; + var childViews: AppView[]; + var nestedChildViews: AppView[]; + beforeEach(() => { + nestedChildProtoView = createEmbeddedPv(); + childProtoView = createEmbeddedPv([ + createNestedElBinder( + createComponentPv([createNestedElBinder(nestedChildProtoView)])) + ]); + var hostProtoView = createHostPv( + [createNestedElBinder(createComponentPv([createNestedElBinder(childProtoView)]))]); + hostView = internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null)); + vcRef = hostView.elementRefs[1]; + nestedChildViews = []; + childViews = []; + nestedVcRefs = []; + for (var i = 0; i < 2; i++) { + var view = internalView( + manager.createViewInContainer(vcRef, i, wrapPv(childProtoView), null)); + childViews.push(view); + var nestedVcRef = view.elementRefs[view.elementOffset]; + nestedVcRefs.push(nestedVcRef); + for (var j = 0; j < 2; j++) { + var nestedView = internalView( + manager.createViewInContainer(nestedVcRef, j, wrapPv(childProtoView), null)); + nestedChildViews.push(nestedView); + } + } + resetSpies(); + }); - it('should detach', () => { - manager.destroyRootHostView(wrapView(parentView)); - expect(utils.spy('detachViewInContainer')).toHaveBeenCalledWith(parentView, 0, 0); - expect(renderer.spy('detachViewInContainer')) - .toHaveBeenCalledWith(parentView.elementRefs[0], 0, childView.render); - }); + it('should dehydrate all child views', () => { + manager.destroyRootHostView(wrapView(hostView)); + childViews.forEach((childView) => expect(childView.hydrated()).toBe(false)); + nestedChildViews.forEach((childView) => expect(childView.hydrated()).toBe(false)); + }); - it('should return the view to the pool', () => { - manager.destroyRootHostView(wrapView(parentView)); - expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView); }); }); @@ -402,18 +475,6 @@ export function main() { }); } -class MockProtoViewRef extends RenderProtoViewRef { - nestedComponentCount: number; - constructor(nestedComponentCount: number) { - super(); - this.nestedComponentCount = nestedComponentCount; - } -} - -@Component({selector: 'someComponent'}) -class SomeComponent { -} - @proxy @IMPLEMENTS(Renderer) class SpyRenderer extends SpyObject { @@ -428,23 +489,9 @@ class SpyAppViewPool extends SpyObject { noSuchMethod(m) { return super.noSuchMethod(m) } } -@proxy -@IMPLEMENTS(AppViewManagerUtils) -class SpyAppViewManagerUtils extends SpyObject { - constructor() { super(AppViewManagerUtils); } - noSuchMethod(m) { return super.noSuchMethod(m) } -} - @proxy @IMPLEMENTS(AppViewListener) class SpyAppViewListener extends SpyObject { constructor() { super(AppViewListener); } noSuchMethod(m) { return super.noSuchMethod(m) } } - -@proxy -@IMPLEMENTS(ElementInjector) -class SpyElementInjector extends SpyObject { - constructor() { super(ElementInjector); } - noSuchMethod(m) { return super.noSuchMethod(m) } -} diff --git a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts index abff81ccbb..800d16173a 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts @@ -14,6 +14,7 @@ import { xit, SpyObject, SpyChangeDetector, + SpyProtoChangeDetector, proxy, Log } from 'angular2/test_lib'; @@ -22,7 +23,7 @@ import {Injector, bind} from 'angular2/di'; import {IMPLEMENTS, isBlank, isPresent} from 'angular2/src/facade/lang'; import {MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; -import {AppProtoView, AppView} from 'angular2/src/core/compiler/view'; +import {AppProtoView, AppView, AppProtoViewMergeMapping} from 'angular2/src/core/compiler/view'; import {ElementBinder} from 'angular2/src/core/compiler/element_binder'; import { DirectiveBinding, @@ -33,89 +34,31 @@ import { import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; import {Component} from 'angular2/annotations'; import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils'; +import {RenderProtoViewMergeMapping, ViewType, RenderViewWithFragments} from 'angular2/render'; export function main() { // TODO(tbosch): add more tests here! describe('AppViewManagerUtils', () => { - var directiveResolver; - var utils; + var utils: AppViewManagerUtils; - function createInjector() { return Injector.resolveAndCreate([]); } + beforeEach(() => { utils = new AppViewManagerUtils(); }); - function createDirectiveBinding(type) { - var annotation = directiveResolver.resolve(type); - return DirectiveBinding.createFromType(type, annotation); + function createViewWithChildren(pv: AppProtoView): AppView { + var renderViewWithFragments = new RenderViewWithFragments(null, [null, null]); + return utils.createView(pv, renderViewWithFragments, null, null); } - function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null); } - - function createComponentElBinder(nestedProtoView = null) { - var binding = createDirectiveBinding(SomeComponent); - var binder = new ElementBinder(0, null, 0, null, binding); - binder.nestedProtoView = nestedProtoView; - return binder; - } - - function createProtoView(binders = null) { - if (isBlank(binders)) { - binders = []; - } - var res = new AppProtoView(null, null, null, null); - res.elementBinders = binders; - return res; - } - - function createElementInjector(parent = null) { - var host = new SpyElementInjector(); - var elementInjector = - isPresent(parent) ? new SpyElementInjectorWithParent(parent) : new SpyElementInjector(); - return SpyObject.stub(elementInjector, - { - 'isExportingComponent': false, - 'isExportingElement': false, - 'getEventEmitterAccessors': [], - 'getHostActionAccessors': [], - 'getComponent': null, - 'getHost': host - }, - {}); - } - - function createView(pv = null, nestedInjectors = false) { - if (isBlank(pv)) { - pv = createProtoView(); - } - var view = new AppView(null, pv, new Map()); - var elementInjectors = ListWrapper.createGrowableSize(pv.elementBinders.length); - var preBuiltObjects = ListWrapper.createFixedSize(pv.elementBinders.length); - for (var i = 0; i < pv.elementBinders.length; i++) { - if (nestedInjectors && i > 0) { - elementInjectors[i] = createElementInjector(elementInjectors[i - 1]); - } else { - elementInjectors[i] = createElementInjector(); - } - preBuiltObjects[i] = new SpyPreBuiltObjects(); - } - view.init(new SpyChangeDetector(), elementInjectors, elementInjectors, preBuiltObjects, - ListWrapper.createFixedSize(pv.elementBinders.length)); - return view; - } - - beforeEach(() => { - directiveResolver = new DirectiveResolver(); - utils = new AppViewManagerUtils(); - }); - - describe("hydrateComponentView", () => { + describe('shared hydrate functionality', () => { it("should hydrate the change detector after hydrating element injectors", () => { var log = new Log(); - var componentView = createView(createProtoView([createEmptyElBinder()])); - var hostView = createView(createProtoView([createComponentElBinder(createProtoView())])); - hostView.componentChildViews = [componentView]; + var componentProtoView = createComponentPv([createEmptyElBinder()]); + var hostView = + createViewWithChildren(createHostPv([createNestedElBinder(componentProtoView)])); + var componentView = hostView.views[1]; var spyEi = componentView.elementInjectors[0]; spyEi.spy('hydrate').andCallFake(log.fn('hydrate')); @@ -123,20 +66,17 @@ export function main() { var spyCd = componentView.changeDetector; spyCd.spy('hydrate').andCallFake(log.fn('hydrateCD')); - utils.hydrateComponentView(hostView, 0); + utils.hydrateRootHostView(hostView, createInjector()); expect(log.result()).toEqual('hydrate; hydrateCD'); }); - }); - - describe('shared hydrate functionality', () => { - it("should set up event listeners", () => { var dir = new Object(); - var hostPv = createProtoView([createComponentElBinder(null), createEmptyElBinder()]); - var hostView = createView(hostPv); + var hostPv = + createHostPv([createNestedElBinder(createComponentPv()), createEmptyElBinder()]); + var hostView = createViewWithChildren(hostPv); var spyEventAccessor1 = SpyObject.stub({"subscribe": null}); SpyObject.stub(hostView.elementInjectors[0], { 'getHostActionAccessors': [], @@ -150,9 +90,6 @@ export function main() { 'getDirectiveAtIndex': dir }); - var shadowView = createView(); - utils.attachComponentView(hostView, 0, shadowView); - utils.hydrateRootHostView(hostView, createInjector()); expect(spyEventAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir); @@ -162,8 +99,9 @@ export function main() { it("should set up host action listeners", () => { var dir = new Object(); - var hostPv = createProtoView([createComponentElBinder(null), createEmptyElBinder()]); - var hostView = createView(hostPv); + var hostPv = + createHostPv([createNestedElBinder(createComponentPv()), createEmptyElBinder()]); + var hostView = createViewWithChildren(hostPv); var spyActionAccessor1 = SpyObject.stub({"subscribe": null}); SpyObject.stub(hostView.elementInjectors[0], { 'getHostActionAccessors': [[spyActionAccessor1]], @@ -177,37 +115,51 @@ export function main() { 'getDirectiveAtIndex': dir }); - var shadowView = createView(); - utils.attachComponentView(hostView, 0, shadowView); - utils.hydrateRootHostView(hostView, createInjector()); expect(spyActionAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir); expect(spyActionAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir); }); + it("should not hydrate element injectors of component views inside of embedded fragments", + () => { + var hostView = createViewWithChildren(createHostPv([ + createNestedElBinder(createComponentPv([ + createNestedElBinder(createEmbeddedPv( + [createNestedElBinder(createComponentPv([createEmptyElBinder()]))])) + ])) + ])); + + utils.hydrateRootHostView(hostView, createInjector()); + expect(hostView.elementInjectors.length).toBe(4); + expect((hostView.elementInjectors[3]).spy('hydrate')).not.toHaveBeenCalled(); + }); + + }); describe('attachViewInContainer', () => { var parentView, contextView, childView; function createViews(numInj = 1) { - var parentPv = createProtoView([createEmptyElBinder()]); - parentView = createView(parentPv); + var childPv = createEmbeddedPv([createEmptyElBinder()]); + childView = createViewWithChildren(childPv); + + var parentPv = createHostPv([createEmptyElBinder()]); + parentView = createViewWithChildren(parentPv); var binders = []; - for (var i = 0; i < numInj; i++) binders.push(createEmptyElBinder()); - var contextPv = createProtoView(binders); - contextView = createView(contextPv, true); - - var childPv = createProtoView([createEmptyElBinder()]); - childView = createView(childPv); + for (var i = 0; i < numInj; i++) { + binders.push(createEmptyElBinder(i > 0 ? binders[i - 1] : null)) + }; + var contextPv = createHostPv(binders); + contextView = createViewWithChildren(contextPv); } it('should link the views rootElementInjectors at the given context', () => { createViews(); utils.attachViewInContainer(parentView, 0, contextView, 0, 0, childView); - expect(contextView.elementInjectors.length).toEqual(2); + expect(contextView.rootElementInjectors.length).toEqual(2); }); it('should link the views rootElementInjectors after the elementInjector at the given context', @@ -223,14 +175,14 @@ export function main() { var parentView, contextView, childView; function createViews() { - var parentPv = createProtoView([createEmptyElBinder()]); - parentView = createView(parentPv); + var parentPv = createHostPv([createEmptyElBinder()]); + parentView = createViewWithChildren(parentPv); - var contextPv = createProtoView([createEmptyElBinder()]); - contextView = createView(contextPv); + var contextPv = createHostPv([createEmptyElBinder()]); + contextView = createViewWithChildren(contextPv); - var childPv = createProtoView([createEmptyElBinder()]); - childView = createView(childPv); + var childPv = createEmbeddedPv([createEmptyElBinder()]); + childView = createViewWithChildren(childPv); utils.attachViewInContainer(parentView, 0, contextView, 0, 0, childView); } @@ -249,8 +201,8 @@ export function main() { var hostView; function createViews() { - var hostPv = createProtoView([createComponentElBinder()]); - hostView = createView(hostPv); + var hostPv = createHostPv([createNestedElBinder(createComponentPv())]); + hostView = createViewWithChildren(hostPv); } it("should instantiate the elementInjectors with the given injector and an empty host element injector", @@ -268,25 +220,125 @@ export function main() { }); } + +export function createInjector() { + return Injector.resolveAndCreate([]); +} + +function createElementInjector(parent = null) { + var host = new SpyElementInjector(null); + var elementInjector = new SpyElementInjector(parent); + return SpyObject.stub(elementInjector, + { + 'isExportingComponent': false, + 'isExportingElement': false, + 'getEventEmitterAccessors': [], + 'getHostActionAccessors': [], + 'getComponent': new Object(), + 'getHost': host + }, + {}); +} + +export function createProtoElInjector(parent: ProtoElementInjector = null): ProtoElementInjector { + var pei = new SpyProtoElementInjector(parent); + pei.spy('instantiate').andCallFake((parentEli) => createElementInjector(parentEli)); + return pei; +} + +export function createEmptyElBinder(parent: ElementBinder = null) { + var parentPeli = isPresent(parent) ? parent.protoElementInjector : null; + return new ElementBinder(0, null, 0, createProtoElInjector(parentPeli), null); +} + +export function createNestedElBinder(nestedProtoView: AppProtoView) { + var componentBinding = null; + if (nestedProtoView.type === ViewType.COMPONENT) { + var annotation = new DirectiveResolver().resolve(SomeComponent); + componentBinding = DirectiveBinding.createFromType(SomeComponent, annotation); + } + var binder = new ElementBinder(0, null, 0, createProtoElInjector(), componentBinding); + binder.nestedProtoView = nestedProtoView; + return binder; +} + +function countNestedElementBinders(pv: AppProtoView): number { + var result = pv.elementBinders.length; + pv.elementBinders.forEach(binder => { + if (isPresent(binder.nestedProtoView)) { + result += countNestedElementBinders(binder.nestedProtoView); + } + }); + return result; +} + +function calcHostElementIndicesByViewIndex(pv: AppProtoView, elementOffset = 0, + target: number[] = null): number[] { + if (isBlank(target)) { + target = [null]; + } + for (var binderIdx = 0; binderIdx < pv.elementBinders.length; binderIdx++) { + var binder = pv.elementBinders[binderIdx]; + if (isPresent(binder.nestedProtoView)) { + target.push(elementOffset + binderIdx); + calcHostElementIndicesByViewIndex(binder.nestedProtoView, + elementOffset + pv.elementBinders.length, target); + elementOffset += countNestedElementBinders(binder.nestedProtoView); + } + } + return target; +} + +function _createProtoView(type: ViewType, binders: ElementBinder[] = null) { + if (isBlank(binders)) { + binders = []; + } + var protoChangeDetector = new SpyProtoChangeDetector(); + protoChangeDetector.spy('instantiate').andReturn(new SpyChangeDetector()); + var res = new AppProtoView(type, protoChangeDetector, null, null, 0); + res.elementBinders = binders; + var mappedElementIndices = ListWrapper.createFixedSize(countNestedElementBinders(res)); + for (var i = 0; i < binders.length; i++) { + var binder = binders[i]; + mappedElementIndices[i] = i; + binder.protoElementInjector.index = i; + } + var hostElementIndicesByViewIndex = calcHostElementIndicesByViewIndex(res); + res.mergeMapping = new AppProtoViewMergeMapping( + new RenderProtoViewMergeMapping(null, hostElementIndicesByViewIndex.length, + mappedElementIndices, [], hostElementIndicesByViewIndex)); + return res; +} + +export function createHostPv(binders: ElementBinder[] = null) { + return _createProtoView(ViewType.HOST, binders); +} + +export function createComponentPv(binders: ElementBinder[] = null) { + return _createProtoView(ViewType.COMPONENT, binders); +} + +export function createEmbeddedPv(binders: ElementBinder[] = null) { + return _createProtoView(ViewType.EMBEDDED, binders); +} + + @Component({selector: 'someComponent'}) class SomeComponent { } @proxy -@IMPLEMENTS(ElementInjector) -class SpyElementInjector extends SpyObject { - constructor() { super(ElementInjector); } +@IMPLEMENTS(ProtoElementInjector) +class SpyProtoElementInjector extends SpyObject { + index: number; + constructor(public parent: ProtoElementInjector) { super(ProtoElementInjector); } noSuchMethod(m) { return super.noSuchMethod(m) } } @proxy @IMPLEMENTS(ElementInjector) -class SpyElementInjectorWithParent extends SpyObject { - parent: ElementInjector; - constructor(parent) { - super(ElementInjector); - this.parent = parent; - } +class SpyElementInjector extends SpyObject { + constructor(public parent: ElementInjector) { super(ElementInjector); } noSuchMethod(m) { return super.noSuchMethod(m) } } diff --git a/modules/angular2/test/core/compiler/view_pool_spec.ts b/modules/angular2/test/core/compiler/view_pool_spec.ts index abfbb61c78..0245b9c4fe 100644 --- a/modules/angular2/test/core/compiler/view_pool_spec.ts +++ b/modules/angular2/test/core/compiler/view_pool_spec.ts @@ -24,9 +24,11 @@ export function main() { function createViewPool({capacity}): AppViewPool { return new AppViewPool(capacity); } - function createProtoView() { return new AppProtoView(null, null, null, null); } + function createProtoView() { return new AppProtoView(null, null, null, null, null); } - function createView(pv) { return new AppView(null, pv, new Map()); } + function createView(pv) { + return new AppView(null, pv, null, null, null, null, new Map(), null, null); + } it('should support multiple AppProtoViews', () => { var vf = createViewPool({capacity: 2}); diff --git a/modules/angular2/test/dom/dom_adapter_spec.ts b/modules/angular2/test/dom/dom_adapter_spec.ts new file mode 100644 index 0000000000..2d867d517b --- /dev/null +++ b/modules/angular2/test/dom/dom_adapter_spec.ts @@ -0,0 +1,53 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + el, + expect, + iit, + inject, + it, + xit, + beforeEachBindings, + SpyObject, + stringifyElement +} from 'angular2/test_lib'; + +import {DOM} from 'angular2/src/dom/dom_adapter'; + +export function main() { + describe('dom adapter', () => { + it('should not coalesque text nodes', () => { + var el1 = el('
a
'); + var el2 = el('
b
'); + DOM.appendChild(el2, DOM.firstChild(el1)); + expect(DOM.childNodes(el2).length).toBe(2); + + var el2Clone = DOM.clone(el2); + expect(DOM.childNodes(el2Clone).length).toBe(2); + }); + + it('should clone correctly', () => { + var el1 = el('
ab
'); + var clone = DOM.clone(el1); + + expect(clone).not.toBe(el1); + DOM.setAttribute(clone, 'test', '1'); + expect(DOM.getOuterHTML(clone)).toEqual('
ab
'); + expect(DOM.getAttribute(el1, 'test')).toBeFalsy(); + + var cNodes = DOM.childNodes(clone); + var firstChild = cNodes[0]; + var secondChild = cNodes[1]; + expect(DOM.parentElement(firstChild)).toBe(clone); + expect(DOM.nextSibling(firstChild)).toBe(secondChild); + expect(DOM.isTextNode(firstChild)).toBe(true); + + expect(DOM.parentElement(secondChild)).toBe(clone); + expect(DOM.nextSibling(secondChild)).toBeFalsy(); + expect(DOM.isElementNode(secondChild)).toBe(true); + + }); + }); +} \ No newline at end of file diff --git a/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts b/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts index fc4808d2cf..378dc12540 100644 --- a/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts +++ b/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts @@ -36,7 +36,7 @@ export function runCompilerCommonTests() { } var tplLoader = new FakeViewLoader(urlData); mockStepFactory = new MockStepFactory([new MockStep(processClosure)]); - return new DomCompiler(mockStepFactory, tplLoader); + return new DomCompiler(mockStepFactory, tplLoader, false); } describe('compile', () => { @@ -64,7 +64,8 @@ export function runCompilerCommonTests() { {id: 'id', selector: 'CUSTOM', type: DirectiveMetadata.COMPONENT_TYPE}); compiler.compileHost(dirMetadata) .then((protoView) => { - expect(DOM.tagName(resolveInternalDomProtoView(protoView.render).element)) + expect(DOM.tagName(DOM.firstChild(DOM.content( + resolveInternalDomProtoView(protoView.render).rootElement)))) .toEqual('CUSTOM'); expect(mockStepFactory.viewDef.directives).toEqual([dirMetadata]); expect(protoView.variableBindings) @@ -79,7 +80,7 @@ export function runCompilerCommonTests() { compiler.compile( new ViewDefinition({componentId: 'someId', template: 'inline component'})) .then((protoView) => { - expect(DOM.getInnerHTML(resolveInternalDomProtoView(protoView.render).element)) + expect(DOM.getInnerHTML(resolveInternalDomProtoView(protoView.render).rootElement)) .toEqual('inline component'); async.done(); }); @@ -90,7 +91,7 @@ export function runCompilerCommonTests() { var compiler = createCompiler(EMPTY_STEP, urlData); compiler.compile(new ViewDefinition({componentId: 'someId', templateAbsUrl: 'someUrl'})) .then((protoView) => { - expect(DOM.getInnerHTML(resolveInternalDomProtoView(protoView.render).element)) + expect(DOM.getInnerHTML(resolveInternalDomProtoView(protoView.render).rootElement)) .toEqual('url component'); async.done(); }); diff --git a/modules/angular2/test/render/dom/compiler/text_interpolation_parser_spec.ts b/modules/angular2/test/render/dom/compiler/text_interpolation_parser_spec.ts index d83e17bae0..b34ce4ee51 100644 --- a/modules/angular2/test/render/dom/compiler/text_interpolation_parser_spec.ts +++ b/modules/angular2/test/render/dom/compiler/text_interpolation_parser_spec.ts @@ -2,9 +2,12 @@ import {describe, beforeEach, expect, it, iit, ddescribe, el} from 'angular2/tes import {TextInterpolationParser} from 'angular2/src/render/dom/compiler/text_interpolation_parser'; import {CompilePipeline} from 'angular2/src/render/dom/compiler/compile_pipeline'; import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; -import {Lexer, Parser} from 'angular2/change_detection'; +import {Lexer, Parser, ASTWithSource} from 'angular2/change_detection'; import {IgnoreChildrenStep} from './pipeline_spec'; -import {ElementBinderBuilder} from 'angular2/src/render/dom/view/proto_view_builder'; +import { + ProtoViewBuilder, + ElementBinderBuilder +} from 'angular2/src/render/dom/view/proto_view_builder'; import {DOM} from 'angular2/src/dom/dom_adapter'; export function main() { @@ -14,48 +17,55 @@ export function main() { [new IgnoreChildrenStep(), new TextInterpolationParser(new Parser(new Lexer()))]); } - function process(element): List { - return ListWrapper.map(createPipeline().process(element), - (compileElement) => compileElement.inheritedElementBinder); + function process(templateString: string): ProtoViewBuilder { + var compileElements = createPipeline().process(DOM.createTemplate(templateString)); + return compileElements[0].inheritedProtoView; } - function assertTextBinding(elementBinder, bindingIndex, nodeIndex, expression) { - expect(elementBinder.textBindings[bindingIndex].source).toEqual(expression); - expect(elementBinder.textBindingNodes[bindingIndex]) - .toEqual(DOM.childNodes(DOM.templateAwareRoot(elementBinder.element))[nodeIndex]); + function assertRootTextBinding(protoViewBuilder: ProtoViewBuilder, nodeIndex: number, + expression: string) { + var node = DOM.childNodes(DOM.templateAwareRoot(protoViewBuilder.rootElement))[nodeIndex]; + expect(protoViewBuilder.rootTextBindings.get(node).source).toEqual(expression); } - it('should find text interpolation in normal elements', () => { - var result = process(el('
{{expr1}}{{expr2}}
'))[0]; - assertTextBinding(result, 0, 0, "{{expr1}}"); - assertTextBinding(result, 1, 2, "{{expr2}}"); + function assertElementTextBinding(elementBinderBuilder: ElementBinderBuilder, nodeIndex: number, + expression: string) { + var node = DOM.childNodes(DOM.templateAwareRoot(elementBinderBuilder.element))[nodeIndex]; + expect(elementBinderBuilder.textBindings.get(node).source).toEqual(expression); + } + + it('should find root text interpolations', () => { + var result = process('{{expr1}}{{expr2}}
{{expr3}}'); + assertRootTextBinding(result, 0, "{{expr1}}{{expr2}}"); + assertRootTextBinding(result, 2, "{{expr3}}"); }); - it('should find text interpolation in template elements', () => { - var result = process(el(''))[0]; - assertTextBinding(result, 0, 0, "{{expr1}}"); - assertTextBinding(result, 1, 2, "{{expr2}}"); + it('should find text interpolation in normal elements', () => { + var result = process('
{{expr1}}{{expr2}}
'); + assertElementTextBinding(result.elements[0], 0, "{{expr1}}"); + assertElementTextBinding(result.elements[0], 2, "{{expr2}}"); }); it('should allow multiple expressions', () => { - var result = process(el('
{{expr1}}{{expr2}}
'))[0]; - assertTextBinding(result, 0, 0, "{{expr1}}{{expr2}}"); + var result = process('
{{expr1}}{{expr2}}
'); + assertElementTextBinding(result.elements[0], 0, "{{expr1}}{{expr2}}"); }); it('should not interpolate when compileChildren is false', () => { - var results = process(el('
{{included}}{{excluded}}
')); - assertTextBinding(results[0], 0, 0, "{{included}}"); - expect(results[1]).toBe(results[0]); + var results = process('
{{included}}{{excluded}}
'); + assertElementTextBinding(results.elements[0], 0, "{{included}}"); + expect(results.elements.length).toBe(1); + expect(results.elements[0].textBindings.size).toBe(1); }); it('should allow fixed text before, in between and after expressions', () => { - var result = process(el('
a{{expr1}}b{{expr2}}c
'))[0]; - assertTextBinding(result, 0, 0, "a{{expr1}}b{{expr2}}c"); + var result = process('
a{{expr1}}b{{expr2}}c
'); + assertElementTextBinding(result.elements[0], 0, "a{{expr1}}b{{expr2}}c"); }); it('should escape quotes in fixed parts', () => { - var result = process(el("
'\"a{{expr1}}
"))[0]; - assertTextBinding(result, 0, 0, "'\"a{{expr1}}"); + var result = process("
'\"a{{expr1}}
"); + assertElementTextBinding(result.elements[0], 0, "'\"a{{expr1}}"); }); }); } diff --git a/modules/angular2/test/render/dom/compiler/view_splitter_spec.ts b/modules/angular2/test/render/dom/compiler/view_splitter_spec.ts index 44cecd1d43..f336735072 100644 --- a/modules/angular2/test/render/dom/compiler/view_splitter_spec.ts +++ b/modules/angular2/test/render/dom/compiler/view_splitter_spec.ts @@ -27,7 +27,7 @@ export function main() { describe('