From 2e21690c664a6c038c0e26638e37b2a610513011 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Tue, 22 May 2018 17:40:59 +0200 Subject: [PATCH] feat(ivy): support renderer.destroy and renderer.destroyNode hooks (#24049) PR Close #24049 --- packages/core/src/render3/STATUS.md | 33 +++++- packages/core/src/render3/ng_dev_mode.ts | 4 + .../core/src/render3/node_manipulation.ts | 18 ++- packages/core/test/render3/render_util.ts | 8 +- .../test/render3/renderer_factory_spec.ts | 111 +++++++++++++++++- 5 files changed, 162 insertions(+), 12 deletions(-) diff --git a/packages/core/src/render3/STATUS.md b/packages/core/src/render3/STATUS.md index a96a211e99..3f492db4f0 100644 --- a/packages/core/src/render3/STATUS.md +++ b/packages/core/src/render3/STATUS.md @@ -228,10 +228,10 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S ### View Encapsulation | Feature | Runtime | Spec | Compiler | | ----------------------------------- | ------- | -------- | -------- | -| Render3.None | ✅ | ✅ | ✅ | -| Render2.None | ✅ | ✅ | ✅ | -| Render2.Emulated | ❌ | ❌ | ❌ | -| Render2.Native | ❌ | ❌ | ❌ | +| Renderer3.None | ✅ | ✅ | ✅ | +| Renderer2.None | ✅ | ✅ | ✅ | +| Renderer2.Emulated | ❌ | ❌ | ❌ | +| Renderer2.Native | ❌ | ❌ | ❌ | @@ -254,3 +254,28 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S | `checkNoChanges()` | n/a | n/a | ❌ | n/a | n/a | ✅ | | `reattach()` | n/a | n/a | ❌ | n/a | n/a | ✅ | | `nativeElement()` | n/a | n/a | n/a | n/a | ✅ | n/a | + +### Renderer2 +| Method | Runtime | +| ----------------------------------- | ------- | +| `data()` | n/a | +| `destroy()` | ✅ | +| `createElement()` | ✅ | +| `createComment()` | n/a | +| `createText()` | ✅ | +| `destroyNode()` | ✅ | +| `appendChild()` | ✅ | +| `insertBefore()` | ✅ | +| `removeChild()` | ✅ | +| `selectRootElement()` | ✅ | +| `parentNode()` | ❌ | +| `nextSibling()` | ❌ | +| `setAttribute()` | ✅ | +| `removeAttribute()` | ✅ | +| `addClass()` | ✅ | +| `removeClass()` | ✅ | +| `setStyle()` | ✅ | +| `removeStyle()` | ✅ | +| `setProperty()` | ✅ | +| `setValue()` | ✅ | +| `listen()` | ✅ | \ No newline at end of file diff --git a/packages/core/src/render3/ng_dev_mode.ts b/packages/core/src/render3/ng_dev_mode.ts index 3580605386..e3a2d7188b 100644 --- a/packages/core/src/render3/ng_dev_mode.ts +++ b/packages/core/src/render3/ng_dev_mode.ts @@ -25,6 +25,8 @@ declare global { rendererRemoveClass: number; rendererSetStyle: number; rendererRemoveStyle: number; + rendererDestroy: number; + rendererDestroyNode: number; } } @@ -50,6 +52,8 @@ export const ngDevModeResetPerfCounters: () => void = rendererRemoveClass: 0, rendererSetStyle: 0, rendererRemoveStyle: 0, + rendererDestroy: 0, + rendererDestroyNode: 0, }; } ngDevModeResetPerfCounters(); diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index cb0bc5bdee..1000197b31 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -9,7 +9,7 @@ import {assertNotNull} from './assert'; import {callHooks} from './hooks'; import {LContainer, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; -import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; +import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {HookData, LView, LViewOrLContainer, TView, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; @@ -215,8 +215,15 @@ export function addRemoveViewFromContainer( renderer.insertBefore(parent, node.native !, beforeNode as RNode | null) : parent.insertBefore(node.native !, beforeNode as RNode | null, true); } else { - isProceduralRenderer(renderer) ? renderer.removeChild(parent as RElement, node.native !) : - parent.removeChild(node.native !); + if (isProceduralRenderer(renderer)) { + renderer.removeChild(parent as RElement, node.native !); + if (renderer.destroyNode) { + ngDevMode && ngDevMode.rendererDestroyNode++; + renderer.destroyNode(node.native !); + } + } else { + parent.removeChild(node.native !); + } } nextNode = getNextLNode(node); } else if (node.tNode.type === TNodeType.Container) { @@ -398,6 +405,11 @@ function cleanUpView(view: LView): void { removeListeners(view); executeOnDestroys(view); executePipeOnDestroys(view); + // For component views only, the local renderer is destroyed as clean up time. + if (view.id === -1 && isProceduralRenderer(view.renderer)) { + ngDevMode && ngDevMode.rendererDestroy++; + view.renderer.destroy(); + } } /** Removes listeners and unsubscribes from output subscriptions */ diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 655ff053d5..7bb954e2cd 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -53,6 +53,7 @@ export class TemplateFixture extends BaseFixture { private _directiveDefs: DirectiveDefList|null; private _pipeDefs: PipeDefList|null; private _sanitizer: Sanitizer|null; + private _rendererFactory: RendererFactory3; /** * @@ -64,11 +65,12 @@ export class TemplateFixture extends BaseFixture { constructor( private createBlock: () => void, private updateBlock: () => void = noop, directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null, - sanitizer?: Sanitizer) { + sanitizer?: Sanitizer|null, rendererFactory?: RendererFactory3) { super(); this._directiveDefs = toDefs(directives, extractDirectiveDef); this._pipeDefs = toDefs(pipes, extractPipeDef); this._sanitizer = sanitizer || null; + this._rendererFactory = rendererFactory || domRendererFactory3; this.hostNode = renderTemplate(this.hostElement, (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { this.createBlock(); @@ -76,7 +78,7 @@ export class TemplateFixture extends BaseFixture { if (rf & RenderFlags.Update) { this.updateBlock(); } - }, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs, sanitizer); + }, null !, this._rendererFactory, null, this._directiveDefs, this._pipeDefs, sanitizer); } /** @@ -86,7 +88,7 @@ export class TemplateFixture extends BaseFixture { */ update(updateBlock?: () => void): void { renderTemplate( - this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3, + this.hostNode.native, updateBlock || this.updateBlock, null !, this._rendererFactory, this.hostNode, this._directiveDefs, this._pipeDefs, this._sanitizer); } } diff --git a/packages/core/test/render3/renderer_factory_spec.ts b/packages/core/test/render3/renderer_factory_spec.ts index 68cbe30ba5..c2d8200a15 100644 --- a/packages/core/test/render3/renderer_factory_spec.ts +++ b/packages/core/test/render3/renderer_factory_spec.ts @@ -11,12 +11,12 @@ import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/brow import {RendererType2, ViewEncapsulation} from '../../src/core'; import {defineComponent, detectChanges} from '../../src/render3/index'; -import {bind, elementEnd, elementProperty, elementStart, listener, text, tick} from '../../src/render3/instructions'; +import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, text, tick} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {createRendererType2} from '../../src/view/index'; import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2'; -import {containerEl, document, renderComponent, renderToHtml, toHtml} from './render_util'; +import {TemplateFixture, containerEl, document, renderComponent, renderToHtml, toHtml} from './render_util'; describe('renderer factory lifecycle', () => { let logs: string[] = []; @@ -206,3 +206,110 @@ describe('animation renderer factory', () => { }); }); }); + +describe('Renderer2 destruction hooks', () => { + const rendererFactory = getRendererFactory2(document); + const origCreateRenderer = rendererFactory.createRenderer; + rendererFactory.createRenderer = function() { + const renderer = origCreateRenderer.apply(this, arguments); + renderer.destroyNode = () => {}; + return renderer; + }; + + it('should call renderer.destroyNode for each node destroyed', () => { + let condition = true; + + function createTemplate() { + elementStart(0, 'div'); + { container(1); } + elementEnd(); + } + + function updateTemplate() { + containerRefreshStart(1); + { + if (condition) { + let rf1 = embeddedViewStart(1); + { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + elementEnd(); + elementStart(1, 'span'); + elementEnd(); + elementStart(2, 'span'); + elementEnd(); + } + } + embeddedViewEnd(); + } + } + containerRefreshEnd(); + } + + const t = + new TemplateFixture(createTemplate, updateTemplate, null, null, null, rendererFactory); + + expect(t.html).toEqual('
'); + + condition = false; + t.update(); + expect(t.html).toEqual('
'); + expect(ngDevMode).toHaveProperties({rendererDestroy: 0, rendererDestroyNode: 3}); + }); + + it('should call renderer.destroy for each component destroyed', () => { + class SimpleComponent { + static ngComponentDef = defineComponent({ + type: SimpleComponent, + selectors: [['simple']], + template: function(rf: RenderFlags, ctx: SimpleComponent) { + if (rf & RenderFlags.Create) { + elementStart(0, 'span'); + elementEnd(); + } + }, + factory: () => new SimpleComponent, + }); + } + + let condition = true; + + function createTemplate() { + elementStart(0, 'div'); + { container(1); } + elementEnd(); + } + + function updateTemplate() { + containerRefreshStart(1); + { + if (condition) { + let rf1 = embeddedViewStart(1); + { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'simple'); + elementEnd(); + elementStart(1, 'span'); + elementEnd(); + elementStart(2, 'simple'); + elementEnd(); + } + } + embeddedViewEnd(); + } + } + containerRefreshEnd(); + } + + const t = new TemplateFixture( + createTemplate, updateTemplate, [SimpleComponent], null, null, rendererFactory); + + expect(t.html).toEqual( + '
'); + + condition = false; + t.update(); + expect(t.html).toEqual('
'); + expect(ngDevMode).toHaveProperties({rendererDestroy: 2, rendererDestroyNode: 3}); + }); +});