diff --git a/modules/@angular/compiler/src/view_compiler/view_builder.ts b/modules/@angular/compiler/src/view_compiler/view_builder.ts index dfac714a94..c290b924f3 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -156,13 +156,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor { var parentRenderNode = this._getParentRenderNode(parent); if (parentRenderNode !== o.NULL_EXPR) { this.view.createMethod.addStmt( - ViewProperties.renderer - .callMethod( - 'projectNodes', - [ - parentRenderNode, - o.THIS_EXPR.callMethod('projectedNodes', [o.literal(ast.index)]) - ]) + o.THIS_EXPR.callMethod('projectNodes', [parentRenderNode, o.literal(ast.index)]) .toStmt()); } else if (this._isRootNode(parent)) { if (this.view.viewType !== ViewType.COMPONENT) { @@ -533,13 +527,17 @@ function generateCreateMethod(view: CompileView): o.Statement[] { } else { resultExpr = o.NULL_EXPR; } + let allNodesExpr: o.Expression = + ViewProperties.renderer.prop('directRenderer') + .conditional(o.NULL_EXPR, o.literalArr(view.nodes.map(node => node.renderNode))); + return parentRenderNodeStmts.concat(view.createMethod.finish(), [ o.THIS_EXPR .callMethod( 'init', [ view.lastRenderNode, - o.literalArr(view.nodes.map(node => node.renderNode)), + allNodesExpr, view.disposables.length ? o.literalArr(view.disposables) : o.NULL_EXPR, ]) .toStmt(), diff --git a/modules/@angular/core/src/core_private_export.ts b/modules/@angular/core/src/core_private_export.ts index b63533cb39..0e260e6fa2 100644 --- a/modules/@angular/core/src/core_private_export.ts +++ b/modules/@angular/core/src/core_private_export.ts @@ -75,7 +75,9 @@ export var __core_private__: { UNINITIALIZED: typeof change_detection_util.UNINITIALIZED, ValueUnwrapper: typeof change_detection_util.ValueUnwrapper, _ValueUnwrapper?: change_detection_util.ValueUnwrapper, - RenderDebugInfo: typeof api.RenderDebugInfo, _RenderDebugInfo?: api.RenderDebugInfo, + RenderDebugInfo: typeof api.RenderDebugInfo, + _RenderDebugInfo?: api.RenderDebugInfo, + _DirectRenderer?: api.DirectRenderer, TemplateRef_: typeof template_ref.TemplateRef_, _TemplateRef_?: template_ref.TemplateRef_, ReflectionCapabilities: typeof reflection_capabilities.ReflectionCapabilities, _ReflectionCapabilities?: reflection_capabilities.ReflectionCapabilities, diff --git a/modules/@angular/core/src/debug/debug_renderer.ts b/modules/@angular/core/src/debug/debug_renderer.ts index a4b259e941..6a22dbf3a5 100644 --- a/modules/@angular/core/src/debug/debug_renderer.ts +++ b/modules/@angular/core/src/debug/debug_renderer.ts @@ -89,6 +89,7 @@ export class DebugDomRenderer implements Renderer { } destroyView(hostElement: any, viewAllNodes: any[]) { + viewAllNodes = viewAllNodes || []; viewAllNodes.forEach((node) => { removeDebugNodeFromIndex(getDebugNode(node)); }); this._delegate.destroyView(hostElement, viewAllNodes); } diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index 87b0b06b6a..995af6a704 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -11,7 +11,7 @@ import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; import {ListWrapper} from '../facade/collection'; import {isPresent} from '../facade/lang'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from '../profile/profile'; -import {RenderComponentType, RenderDebugInfo, Renderer} from '../render/api'; +import {DirectRenderer, RenderComponentType, RenderDebugInfo, Renderer} from '../render/api'; import {AnimationViewContext} from './animation_view_context'; import {ComponentRef} from './component_factory'; @@ -51,6 +51,7 @@ export abstract class AppView { private _hostInjector: Injector; private _hostProjectableNodes: any[][]; private _animationContext: AnimationViewContext; + private _directRenderer: DirectRenderer; public context: T; @@ -64,6 +65,7 @@ export abstract class AppView { } else { this.renderer = parentView.renderer; } + this._directRenderer = (this.renderer as any).directRenderer; } get animationContext(): AnimationViewContext { @@ -136,7 +138,7 @@ export abstract class AppView { detachAndDestroy() { if (this._hasExternalHostElement) { - this.renderer.detachView(this.flatRootNodes); + this.detach(); } else if (isPresent(this.viewContainerElement)) { this.viewContainerElement.detachView(this.viewContainerElement.nestedViews.indexOf(this)); } @@ -179,13 +181,34 @@ export abstract class AppView { detach(): void { this.detachInternal(); if (this._animationContext) { - this._animationContext.onAllActiveAnimationsDone( - () => this.renderer.detachView(this.flatRootNodes)); + this._animationContext.onAllActiveAnimationsDone(() => this._renderDetach()); + } else { + this._renderDetach(); + } + } + + private _renderDetach() { + if (this._directRenderer) { + this.visitRootNodesInternal(this._directRenderer.remove, null); } else { this.renderer.detachView(this.flatRootNodes); } } + attachAfter(prevNode: any) { + if (this._directRenderer) { + const nextSibling = this._directRenderer.nextSibling(prevNode); + if (nextSibling) { + this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling); + } else { + this.visitRootNodesInternal( + this._directRenderer.appendChild, this._directRenderer.parentElement(prevNode)); + } + } else { + this.renderer.attachViewAfter(prevNode, this.flatRootNodes); + } + } + get changeDetectorRef(): ChangeDetectorRef { return this.ref; } get flatRootNodes(): any[] { @@ -194,10 +217,14 @@ export abstract class AppView { return nodes; } - projectedNodes(ngContentIndex: number): any[] { - const nodes: any[] = []; - this.visitProjectedNodes(ngContentIndex, addToArray, nodes); - return nodes; + projectNodes(parentElement: any, ngContentIndex: number) { + if (this._directRenderer) { + this.visitProjectedNodes(ngContentIndex, this._directRenderer.appendChild, parentElement); + } else { + const nodes: any[] = []; + this.visitProjectedNodes(ngContentIndex, addToArray, nodes); + this.renderer.projectNodes(parentElement, nodes); + } } visitProjectedNodes(ngContentIndex: number, cb: (node: any, ctx: C) => void, c: C): void { diff --git a/modules/@angular/core/src/linker/view_container.ts b/modules/@angular/core/src/linker/view_container.ts index fd9f87d2a3..485dbb28ff 100644 --- a/modules/@angular/core/src/linker/view_container.ts +++ b/modules/@angular/core/src/linker/view_container.ts @@ -90,7 +90,7 @@ export class ViewContainer { refRenderNode = this.nativeElement; } if (isPresent(refRenderNode)) { - view.renderer.attachViewAfter(refRenderNode, view.flatRootNodes); + view.attachAfter(refRenderNode); } view.markContentChildAsMoved(this); } @@ -118,7 +118,7 @@ export class ViewContainer { refRenderNode = this.nativeElement; } if (isPresent(refRenderNode)) { - view.renderer.attachViewAfter(refRenderNode, view.flatRootNodes); + view.attachAfter(refRenderNode); } view.addToContentChildren(this); } diff --git a/modules/@angular/core/src/render/api.ts b/modules/@angular/core/src/render/api.ts index 41b0d515d3..baffd7a372 100644 --- a/modules/@angular/core/src/render/api.ts +++ b/modules/@angular/core/src/render/api.ts @@ -33,6 +33,14 @@ export abstract class RenderDebugInfo { get source(): string { return unimplemented(); } } +export interface DirectRenderer { + remove(node: any): void; + appendChild(node: any, parent: any): void; + insertBefore(node: any, refNode: any): void; + nextSibling(node: any): any; + parentElement(node: any): any; +} + /** * @experimental */ diff --git a/modules/@angular/core/test/linker/direct_rendering_integration_spec.ts b/modules/@angular/core/test/linker/direct_rendering_integration_spec.ts new file mode 100644 index 0000000000..a77e5f8edf --- /dev/null +++ b/modules/@angular/core/test/linker/direct_rendering_integration_spec.ts @@ -0,0 +1,144 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; +import {DebugDomRenderer} from '@angular/core/src/debug/debug_renderer'; +import {DirectRenderer} from '@angular/core/src/render/api'; +import {TestBed, inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; +import {DIRECT_DOM_RENDERER, DomRootRenderer} from '@angular/platform-browser/src/dom/dom_renderer'; +import {expect} from '@angular/platform-browser/testing/matchers'; + +let directRenderer: any; +let destroyViewLogs: any[]; + +export function main() { + // Don't run on server... + if (!getDOM().supportsDOMEvents()) return; + describe('direct dom integration tests', function() { + + beforeEach(() => { + directRenderer = DIRECT_DOM_RENDERER; + destroyViewLogs = []; + spyOn(directRenderer, 'remove').and.callThrough(); + spyOn(directRenderer, 'appendChild').and.callThrough(); + spyOn(directRenderer, 'insertBefore').and.callThrough(); + + TestBed.configureTestingModule( + {providers: [{provide: RootRenderer, useClass: DirectRootRenderer}]}); + }); + + it('should attach views as last nodes in a parent', () => { + @Component({template: '
hello
'}) + class MyComp { + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + + const fixture = TestBed.createComponent(MyComp); + + directRenderer.insertBefore.calls.reset(); + directRenderer.appendChild.calls.reset(); + + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('hello'); + expect(directRenderer.insertBefore).not.toHaveBeenCalled(); + expect(directRenderer.appendChild).toHaveBeenCalled(); + }); + + it('should attach views as non last nodes in a parent', () => { + @Component({template: '
hello
after'}) + class MyComp { + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + + const fixture = TestBed.createComponent(MyComp); + + directRenderer.insertBefore.calls.reset(); + directRenderer.appendChild.calls.reset(); + + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('helloafter'); + expect(directRenderer.insertBefore).toHaveBeenCalled(); + expect(directRenderer.appendChild).not.toHaveBeenCalled(); + }); + + it('should detach views', () => { + @Component({template: '
hello
'}) + class MyComp { + shown = true; + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + directRenderer.remove.calls.reset(); + + fixture.componentInstance.shown = false; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText(''); + expect(directRenderer.remove).toHaveBeenCalled(); + }); + + it('should pass null as all nodes to destroyView', () => { + @Component({template: '
hello
'}) + class MyComp { + shown = true; + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + destroyViewLogs.splice(0, destroyViewLogs.length); + + fixture.componentInstance.shown = false; + fixture.detectChanges(); + + expect(destroyViewLogs).toEqual([[null, null]]); + }); + + it('should project nodes', () => { + @Component({template: 'hello'}) + class Parent { + } + + @Component({selector: 'child', template: '()'}) + class Child { + } + + TestBed.configureTestingModule({declarations: [Parent, Child]}); + const fixture = TestBed.createComponent(Parent); + + expect(fixture.nativeElement).toHaveText('(hello)'); + const childHostEl = fixture.nativeElement.children[0]; + const projectedNode = childHostEl.childNodes[1]; + expect(directRenderer.appendChild).toHaveBeenCalledWith(projectedNode, childHostEl); + }); + }); +} + +@Injectable() +class DirectRootRenderer implements RootRenderer { + constructor(private _delegate: DomRootRenderer) {} + renderComponent(componentType: RenderComponentType): Renderer { + const renderer = new DebugDomRenderer(this._delegate.renderComponent(componentType)); + (renderer as any).directRenderer = directRenderer; + const originalDestroyView = renderer.destroyView; + renderer.destroyView = function(...args: any[]) { + destroyViewLogs.push(args); + return originalDestroyView.apply(this, args); + }; + return renderer; + } +} diff --git a/modules/@angular/platform-browser/src/dom/dom_renderer.ts b/modules/@angular/platform-browser/src/dom/dom_renderer.ts index 1c0d440750..792eed3600 100644 --- a/modules/@angular/platform-browser/src/dom/dom_renderer.ts +++ b/modules/@angular/platform-browser/src/dom/dom_renderer.ts @@ -9,7 +9,7 @@ import {APP_ID, Inject, Injectable, RenderComponentType, Renderer, RootRenderer, ViewEncapsulation} from '@angular/core'; import {isBlank, isPresent, stringify} from '../facade/lang'; -import {AnimationKeyframe, AnimationPlayer, AnimationStyles, RenderDebugInfo} from '../private_import_core'; +import {AnimationKeyframe, AnimationPlayer, AnimationStyles, DirectRenderer, RenderDebugInfo} from '../private_import_core'; import {AnimationDriver} from './animation_driver'; import {DOCUMENT} from './dom_tokens'; @@ -54,11 +54,21 @@ export class DomRootRenderer_ extends DomRootRenderer { } } +export const DIRECT_DOM_RENDERER: DirectRenderer = { + remove(node: Text | Comment | Element) { node.remove();}, + appendChild(node: Node, parent: Element) { parent.appendChild(node);}, + insertBefore(node: Node, refNode: Node) { refNode.parentElement.insertBefore(node, refNode);}, + nextSibling(node: Node) { return node.nextSibling;}, + parentElement(node: Node): Element{return node.parentElement;} +}; + export class DomRenderer implements Renderer { private _contentAttr: string; private _hostAttr: string; private _styles: string[]; + directRenderer: DirectRenderer = DIRECT_DOM_RENDERER; + constructor( private _rootRenderer: DomRootRenderer, private componentProto: RenderComponentType, private _animationDriver: AnimationDriver, styleShimId: string) { diff --git a/modules/@angular/platform-browser/src/private_import_core.ts b/modules/@angular/platform-browser/src/private_import_core.ts index f2512b86fb..0353db607a 100644 --- a/modules/@angular/platform-browser/src/private_import_core.ts +++ b/modules/@angular/platform-browser/src/private_import_core.ts @@ -10,6 +10,7 @@ import {__core_private__ as r} from '@angular/core'; export type RenderDebugInfo = typeof r._RenderDebugInfo; export var RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo; +export type DirectRenderer = typeof r._DirectRenderer; export var ReflectionCapabilities: typeof r.ReflectionCapabilities = r.ReflectionCapabilities;