From 87419097da597f6fecc98aa9100a6c8d89cd1ac7 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Tue, 17 Jul 2018 11:45:49 -0700 Subject: [PATCH] fix(ivy): flatten template fns for nested views (#24943) PR Close #24943 --- packages/core/src/render3/component.ts | 2 + packages/core/src/render3/di.ts | 9 +- packages/core/src/render3/instructions.ts | 142 +++++++-- .../core/src/render3/interfaces/definition.ts | 2 +- packages/core/src/render3/interfaces/view.ts | 40 ++- .../test/render3/common_integration_spec.ts | 300 ++++++++++++++++-- packages/core/test/render3/component_spec.ts | 150 ++++++++- packages/core/test/render3/content_spec.ts | 42 +-- .../test/render3/view_container_ref_spec.ts | 223 ++++++++++++- 9 files changed, 809 insertions(+), 101 deletions(-) diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 2888fa3138..c82b66aa1f 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -122,6 +122,8 @@ export function renderComponent( // Create directive instance with factory() and store at index 0 in directives array rootContext.components.push( component = baseDirectiveCreate(0, componentDef.factory(), componentDef) as T); + + (elementNode.data as LViewData)[CONTEXT] = component; initChangeDetectorIfExisting(elementNode.nodeInjector, component, elementNode.data !); opts.hostFeatures && opts.hostFeatures.forEach((feature) => feature(component, componentDef)); diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index a9cbffc999..456417386c 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -24,7 +24,7 @@ import {LInjector} from './interfaces/injector'; import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TContainerNode, TElementNode, TNodeFlags, TNodeType} from './interfaces/node'; import {LQueries, QueryReadType} from './interfaces/query'; import {Renderer3} from './interfaces/renderer'; -import {DIRECTIVES, HOST_NODE, INJECTOR, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view'; +import {DECLARATION_PARENT, DIRECTIVES, HOST_NODE, INJECTOR, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {addRemoveViewFromContainer, appendChild, detachView, getChildLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {ViewRef} from './view_ref'; @@ -728,7 +728,7 @@ export function getOrCreateTemplateRef(di: LInjector): viewEngine.TemplateRef const hostTNode = hostNode.tNode; ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated'); di.templateRef = new TemplateRef( - getOrCreateElementRef(di), hostTNode.tViews as TView, getRenderer(), + hostNode.view, getOrCreateElementRef(di), hostTNode.tViews as TView, getRenderer(), hostNode.data[QUERIES]); } return di.templateRef; @@ -738,14 +738,15 @@ class TemplateRef implements viewEngine.TemplateRef { readonly elementRef: viewEngine.ElementRef; constructor( - elementRef: viewEngine.ElementRef, private _tView: TView, private _renderer: Renderer3, + private _declarationParentView: LViewData, elementRef: viewEngine.ElementRef, private _tView: TView, private _renderer: Renderer3, private _queries: LQueries|null) { this.elementRef = elementRef; } createEmbeddedView(context: T, containerNode?: LContainerNode, index?: number): viewEngine.EmbeddedViewRef { - const viewNode = createEmbeddedViewNode(this._tView, context, this._renderer, this._queries); + const viewNode = createEmbeddedViewNode( + this._tView, context, this._declarationParentView, this._renderer, this._queries); if (containerNode) { insertView(containerNode, viewNode, index !); } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 2f9a5c6c59..ba0c09c8d3 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -22,7 +22,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LEleme import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; -import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view'; +import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_PARENT, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; @@ -253,11 +253,13 @@ export function leaveView(newView: LViewData, creationOnly?: boolean): void { /** * Refreshes the view, executing the following steps in that order: * triggers init hooks, refreshes dynamic embedded views, triggers content hooks, sets host - * bindings, - * refreshes child components. + * bindings, refreshes child components. * Note: view hooks are triggered later when leaving the view. */ -function refreshView() { +function refreshDescendantViews() { + // This needs to be set before children are processed to support recursive components + tView.firstTemplatePass = firstTemplatePass = false; + if (!checkNoChangesMode) { executeInitHooks(viewData, tView, creationMode); } @@ -266,9 +268,6 @@ function refreshView() { executeHooks(directives !, tView.contentHooks, tView.contentCheckHooks, creationMode); } - // This needs to be set before children are processed to support recursive components - tView.firstTemplatePass = firstTemplatePass = false; - setHostBindings(tView.hostBindings); refreshContentQueries(tView); refreshChildComponents(tView.components); @@ -302,8 +301,8 @@ function refreshContentQueries(tView: TView): void { /** Refreshes child components in the current view. */ function refreshChildComponents(components: number[] | null): void { if (components != null) { - for (let i = 0; i < components.length; i += 2) { - componentRefresh(components[i], components[i + 1]); + for (let i = 0; i < components.length; i++) { + componentRefresh(components[i]); } } } @@ -335,6 +334,7 @@ export function createLViewData( null, // tail -1, // containerIndex null, // contentQueries + null // declarationParent ]; } @@ -500,7 +500,8 @@ export function renderTemplate( * Such lViewNode will then be renderer with renderEmbeddedTemplate() (see below). */ export function createEmbeddedViewNode( - tView: TView, context: T, renderer: Renderer3, queries?: LQueries | null): LViewNode { + tView: TView, context: T, declarationParent: LViewData, renderer: Renderer3, + queries?: LQueries | null): LViewNode { const _isParent = isParent; const _previousOrParentNode = previousOrParentNode; isParent = true; @@ -508,6 +509,8 @@ export function createEmbeddedViewNode( const lView = createLViewData(renderer, tView, context, LViewFlags.CheckAlways, getCurrentSanitizer()); + lView[DECLARATION_PARENT] = declarationParent; + if (queries) { lView[QUERIES] = queries.createView(); } @@ -544,9 +547,9 @@ export function renderEmbeddedTemplate( oldView = enterView(viewNode.data !, viewNode); namespaceHTML(); - tView.template !(rf, context); + callTemplateWithContexts(rf, context, tView.template !, viewNode.data ![DECLARATION_PARENT] !); if (rf & RenderFlags.Update) { - refreshView(); + refreshDescendantViews(); } else { viewNode.data ![TVIEW].firstTemplatePass = firstTemplatePass = false; } @@ -562,6 +565,99 @@ export function renderEmbeddedTemplate( return viewNode; } +/** + * This function calls the template function of a dynamically created view with + * all of its declaration parent contexts (up the view tree) until it reaches the + * component boundary. + * + * Example: + * + * AppComponent template: + *
    + *
  • {{ item }}
  • + *
+ * + * function AppComponentTemplate(rf, ctx) { + * // instructions + * function ulTemplate(rf, ulCtx, appCtx) {...} + * function liTemplate(rf, liCtx, ulCtx, appCtx) {...} + * } + * + * The ul view's template must be called with its own context and its declaration + * parent, AppComponent. The li view's template must be called with its own context, its + * parent (the ul), and the ul's parent (AppComponent). + * + * Note that a declaration parent is NOT always the same as the insertion parent. Templates + * can be declared in different views than they are used. + * + * @param rf The RenderFlags for this template invocation + * @param context The context for this template + * @param template The template function to call + * @param parent1 The declaration parent of the dynamic view + */ +function callTemplateWithContexts( + rf: RenderFlags, context: any, template: ComponentTemplate, parent1: LViewData): void { + const parent2 = parent1[DECLARATION_PARENT]; + // Calling a function with extra arguments has a VM cost, so only call with necessary args + if (!parent2) return template(rf, context, parent1[CONTEXT]); + + const parent3 = parent2[DECLARATION_PARENT]; + if (!parent3) return template(rf, context, parent1[CONTEXT], parent2[CONTEXT]); + + const parent4 = parent3[DECLARATION_PARENT]; + if (!parent4) { + return template(rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT]); + } + + const parent5 = parent4[DECLARATION_PARENT]; + if (!parent5) { + return template( + rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT]); + } + + const parent6 = parent5[DECLARATION_PARENT]; + if (!parent6) { + return template( + rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT], + parent5[CONTEXT]); + } + + const parent7 = parent6[DECLARATION_PARENT]; + if (!parent7) { + return template( + rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT], + parent5[CONTEXT], parent6[CONTEXT]); + } + + const parent8 = parent7[DECLARATION_PARENT]; + if (!parent8) { + return template( + rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT], + parent5[CONTEXT], parent6[CONTEXT], parent7[CONTEXT]); + } + + const parent9 = parent8[DECLARATION_PARENT]; + if (!parent9) { + return template( + rf, context, parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT], + parent5[CONTEXT], parent6[CONTEXT], parent7[CONTEXT], parent8[CONTEXT]); + } + + // We support up to 8 nesting levels in embedded views before we give up and call apply() + const contexts = [ + parent1[CONTEXT], parent2[CONTEXT], parent3[CONTEXT], parent4[CONTEXT], parent5[CONTEXT], + parent6[CONTEXT], parent7[CONTEXT], parent8[CONTEXT], parent9[CONTEXT] + ]; + + let currentView: LViewData = parent9; + while (currentView[DECLARATION_PARENT]) { + contexts.push(currentView[DECLARATION_PARENT] ![CONTEXT]); + currentView = currentView[DECLARATION_PARENT] !; + } + + tView.template !(rf, context, ...contexts); +} + export function renderComponentOrTemplate( node: LElementNode, hostView: LViewData, componentOrContext: T, template?: ComponentTemplate) { @@ -573,14 +669,14 @@ export function renderComponentOrTemplate( if (template) { namespaceHTML(); template(getRenderFlags(hostView), componentOrContext !); - refreshView(); + refreshDescendantViews(); } else { executeInitAndContentHooks(); // Element was stored at 0 in data and directive was stored at 0 in directives // in renderComponent() setHostBindings(_ROOT_DIRECTIVE_INDICES); - componentRefresh(0, HEADER_OFFSET); + componentRefresh(HEADER_OFFSET); } } finally { if (rendererFactory.end) { @@ -770,9 +866,9 @@ export function resolveDirective( } /** Stores index of component's host element so it will be queued for view refresh during CD. */ -function queueComponentIndexForCheck(dirIndex: number): void { +function queueComponentIndexForCheck(): void { if (firstTemplatePass) { - (tView.components || (tView.components = [])).push(dirIndex, viewData.length - 1); + (tView.components || (tView.components = [])).push(viewData.length - 1); } } @@ -1543,7 +1639,7 @@ function addComponentLogic( viewData, previousOrParentNode.tNode.index as number, createLViewData( rendererFactory.createRenderer(previousOrParentNode.native as RElement, def.rendererType), - tView, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, + tView, instance, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, getCurrentSanitizer())); // We need to set the host node/data here because when the component LNode was created, @@ -1553,7 +1649,7 @@ function addComponentLogic( initChangeDetectorIfExisting(previousOrParentNode.nodeInjector, instance, componentView); - if (firstTemplatePass) queueComponentIndexForCheck(directiveIndex); + if (firstTemplatePass) queueComponentIndexForCheck(); } /** @@ -1914,7 +2010,7 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV /** Marks the end of an embedded view. */ export function embeddedViewEnd(): void { - refreshView(); + refreshDescendantViews(); isParent = false; previousOrParentNode = viewData[HOST_NODE] as LViewNode; leaveView(viewData[PARENT] !); @@ -1927,10 +2023,9 @@ export function embeddedViewEnd(): void { /** * Refreshes components by entering the component view and processing its bindings, queries, etc. * - * @param directiveIndex Directive index in LViewData[DIRECTIVES] * @param adjustedElementIndex Element index in LViewData[] (adjusted for HEADER_OFFSET) */ -export function componentRefresh(directiveIndex: number, adjustedElementIndex: number): void { +export function componentRefresh(adjustedElementIndex: number): void { ngDevMode && assertDataInRange(adjustedElementIndex); const element = viewData[adjustedElementIndex] as LElementNode; ngDevMode && assertNodeType(element, TNodeType.Element); @@ -1940,8 +2035,7 @@ export function componentRefresh(directiveIndex: number, adjustedElementIndex // Only attached CheckAlways components or attached, dirty OnPush components should be checked if (viewAttached(hostView) && hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) { - ngDevMode && assertDataInRange(directiveIndex, directives !); - detectChangesInternal(hostView, element, directives ![directiveIndex]); + detectChangesInternal(hostView, element, hostView[CONTEXT]); } } @@ -2267,7 +2361,7 @@ export function detectChangesInternal( namespaceHTML(); createViewQuery(viewQuery, hostView[FLAGS], component); template(getRenderFlags(hostView), component); - refreshView(); + refreshDescendantViews(); updateViewQuery(viewQuery, component); } finally { leaveView(oldView); diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 27cedbdefb..add5f29cde 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -15,7 +15,7 @@ import {CssSelectorList} from './projection'; * Definition of what a template rendering function should look like. */ export type ComponentTemplate = { - (rf: RenderFlags, ctx: T): void; ngPrivateData?: never; + (rf: RenderFlags, ctx: T, ...parentCtx: ({} | null)[]): void; ngPrivateData?: never; }; /** diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 3ee8b338b9..a55e6ba734 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -17,7 +17,7 @@ import {LQueries} from './query'; import {Renderer3} from './renderer'; /** Size of LViewData's header. Necessary to adjust for it when setting slots. */ -export const HEADER_OFFSET = 16; +export const HEADER_OFFSET = 17; // Below are constants for LViewData indices to help us look up LViewData members // without having to remember the specific indices. @@ -38,6 +38,7 @@ export const SANITIZER = 12; export const TAIL = 13; export const CONTAINER_INDEX = 14; export const CONTENT_QUERIES = 15; +export const DECLARATION_PARENT = 16; /** * `LViewData` stores all of the information needed to process the instructions as @@ -61,6 +62,9 @@ export interface LViewData extends Array { * The parent view is needed when we exit the view and must restore the previous * `LViewData`. Without this, the render method would have to keep a stack of * views as it is recursively rendering templates. + * + * This is also the "insertion" parent for embedded views. This allows us to properly + * destroy embedded views. */ [PARENT]: LViewData|null; @@ -143,7 +147,6 @@ export interface LViewData extends Array { * The tail allows us to quickly add a new state to the end of the view list * without having to propagate starting from the first child. */ - // TODO: replace with global [TAIL]: LViewData|LContainer|null; /** @@ -162,6 +165,32 @@ export interface LViewData extends Array { * be refreshed. */ [CONTENT_QUERIES]: QueryList[]|null; + + /** + * Parent view where this view's template was declared. + * + * Only applicable for dynamically created views. Will be null for inline/component views. + * + * The template for a dynamically created view may be declared in a different view than + * it is inserted. We already track the "insertion parent" (view where the template was + * inserted) in LViewData[PARENT], but we also need access to the "declaration parent" + * (view where the template was declared). Otherwise, we wouldn't be able to call the + * view's template function with the proper contexts. Context should be inherited from + * the declaration parent tree, not the insertion parent tree. + * + * Example (AppComponent template): + * + * <-- declared here --> + * <-- inserted inside this component --> + * + * The above is declared in the AppComponent template, but it will be passed into + * SomeComp and inserted there. In this case, the declaration parent would be the AppComponent, + * but the insertion parent would be SomeComp. When we are removing views, we would want to + * traverse through the insertion parent to clean up listeners. When we are calling the + * template function during change detection, we need the declaration parent to get inherited + * context. + */ + [DECLARATION_PARENT]: LViewData|null; } /** Flags associated with an LView (saved in LViewData[FLAGS]) */ @@ -404,11 +433,10 @@ export interface TView { cleanup: any[]|null; /** - * A list of directive and element indices for child components that will need to be - * refreshed when the current view has finished its check. + * A list of element indices for child components that will need to be + * refreshed when the current view has finished its check. These indices have + * already been adjusted for the HEADER_OFFSET. * - * Even indices: Directive indices - * Odd indices: Element indices (adjusted for LViewData header offset) */ components: number[]|null; diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts index 96d33c039e..49c3705e60 100644 --- a/packages/core/test/render3/common_integration_spec.ts +++ b/packages/core/test/render3/common_integration_spec.ts @@ -10,7 +10,7 @@ import {NgForOfContext} from '@angular/common'; import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; import {AttributeMarker, defineComponent} from '../../src/render3/index'; -import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, interpolation3, listener, load, text, textBinding} from '../../src/render3/instructions'; +import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def'; @@ -40,7 +40,7 @@ describe('@angular/common integration', () => { elementProperty(1, 'ngForOf', bind(myApp.items)); } - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { + function liTemplate(rf1: RenderFlags, row: NgForOfContext, parent: MyApp) { if (rf1 & RenderFlags.Create) { elementStart(0, 'li'); { text(1); } @@ -100,7 +100,7 @@ describe('@angular/common integration', () => { elementProperty(1, 'ngForOf', bind(myApp.items)); } - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { + function liTemplate(rf1: RenderFlags, row: NgForOfContext, parent: MyApp) { if (rf1 & RenderFlags.Create) { elementStart(0, 'li'); { text(1); } @@ -164,7 +164,7 @@ describe('@angular/common integration', () => { elementProperty(3, 'ngForOf', bind(myApp.items)); } - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { + function liTemplate(rf1: RenderFlags, row: NgForOfContext, parent: MyApp) { if (rf1 & RenderFlags.Create) { elementStart(0, 'li'); { text(1); } @@ -203,14 +203,14 @@ describe('@angular/common integration', () => { it('should support multiple levels of embedded templates', () => { /** - *
    - *
  • - * {{item}} + *
      + *
    • + * {{cell}} - {{ row.value }} *
    • *
    */ class MyApp { - items: string[] = ['1', '2']; + items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; static ngComponentDef = defineComponent({ type: MyApp, @@ -226,25 +226,27 @@ describe('@angular/common integration', () => { elementProperty(1, 'ngForOf', bind(myApp.items)); } - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { + function liTemplate(rf1: RenderFlags, row: any, myApp: MyApp) { if (rf1 & RenderFlags.Create) { elementStart(0, 'li'); { container(1, spanTemplate, null, ['ngForOf', '']); } elementEnd(); } if (rf1 & RenderFlags.Update) { - elementProperty(1, 'ngForOf', bind(myApp.items)); + const r1 = row.$implicit as any; + elementProperty(1, 'ngForOf', bind(r1.data)); } } - function spanTemplate(rf1: RenderFlags, row: NgForOfContext) { + function spanTemplate(rf1: RenderFlags, cell: any, row: any, myApp: MyApp) { if (rf1 & RenderFlags.Create) { elementStart(0, 'span'); { text(1); } elementEnd(); } if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); + textBinding( + 1, interpolation2('', cell.$implicit, ' - ', (row.$implicit as any).value, '')); } } }, @@ -258,25 +260,283 @@ describe('@angular/common integration', () => { fixture.update(); expect(fixture.html) .toEqual( - '
    • 12
    • 12
    '); + '
    • 1 - first2 - first
    • 3 - second4 - second
    '); // Remove the last item fixture.component.items.length = 1; fixture.update(); - expect(fixture.html).toEqual('
    • 1
    '); + expect(fixture.html) + .toEqual('
    • 1 - first2 - first
    '); // Change an item - fixture.component.items[0] = 'one'; + fixture.component.items[0].data[0] = 'one'; fixture.update(); - expect(fixture.html).toEqual('
    • one
    '); + expect(fixture.html) + .toEqual('
    • one - first2 - first
    '); // Add an item - fixture.component.items.push('two'); + fixture.component.items[1] = {data: ['three', '4'], value: 'third'}; fixture.update(); expect(fixture.html) .toEqual( - '
    • onetwo
    • onetwo
    '); + '
    • one - first2 - first
    • three - third4 - third
    '); }); + + it('should support context for 9+ levels of embedded templates', () => { + /** + * + * + * + * + * + * + * + * + * + * + * {{ item8 }} - {{ item7.value }} - {{ item6.value }}... + * + * + * + * + * + * + * + * + * + */ + class MyApp { + value = 'App'; + items: any[] = [ + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '1', '2' // item8 + ], + value: 'h' + }], + value: 'g' + }], + value: 'f' + }], + value: 'e' + }], + value: 'd' + }], + value: 'c' + }], + value: 'b' + }], + value: 'a' + }, + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '3', '4' // item8 + ], + value: 'H' + }], + value: 'G' + }], + value: 'F' + }], + value: 'E' + }], + value: 'D' + }], + value: 'C' + }], + value: 'B' + }], + value: 'A' + } + ]; + + static ngComponentDef = defineComponent({ + type: MyApp, + factory: () => new MyApp(), + selectors: [['my-app']], + template: (rf: RenderFlags, myApp: MyApp) => { + if (rf & RenderFlags.Create) { + container(0, itemTemplate0, null, ['ngForOf', '']); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'ngForOf', bind(myApp.items)); + } + + function itemTemplate0(rf1: RenderFlags, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate1, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item0.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate1(rf1: RenderFlags, item1: any, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate2, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item1.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate2( + rf1: RenderFlags, item2: any, item1: any, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate3, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item2.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate3( + rf1: RenderFlags, item3: any, item2: any, item1: any, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate4, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item3.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate4( + rf1: RenderFlags, item4: any, item3: any, item2: any, item1: any, item0: any, + myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate5, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item4.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate5( + rf1: RenderFlags, item5: any, item4: any, item3: any, item2: any, item1: any, + item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate6, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item5.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate6( + rf1: RenderFlags, item6: any, item5: any, item4: any, item3: any, item2: any, + item1: any, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate7, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item6.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate7( + rf1: RenderFlags, item7: any, item6: any, item5: any, item4: any, item3: any, + item2: any, item1: any, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { container(1, itemTemplate8, null, ['ngForOf', '']); } + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + const item = item7.$implicit as any; + elementProperty(1, 'ngForOf', bind(item.data)); + } + } + + function itemTemplate8( + rf1: RenderFlags, item8: any, item7: any, item6: any, item5: any, item4: any, + item3: any, item2: any, item1: any, item0: any, myApp: MyApp) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'span'); + { text(1); } + elementEnd(); + } + + if (rf1 & RenderFlags.Update) { + textBinding(1, interpolationV([ + '', item8.$implicit, '.', item7.$implicit.value, + '.', item6.$implicit.value, '.', item5.$implicit.value, + '.', item4.$implicit.value, '.', item3.$implicit.value, + '.', item2.$implicit.value, '.', item1.$implicit.value, + '.', item0.$implicit.value, '.', myApp.value, + '' + ])); + } + } + }, + directives: () => [NgForOf] + }); + } + + const fixture = new ComponentFixture(MyApp); + + expect(fixture.html) + .toEqual( + '' + + '1.h.g.f.e.d.c.b.a.App' + + '2.h.g.f.e.d.c.b.a.App' + + '' + + '' + + '3.H.G.F.E.D.C.B.A.App' + + '4.H.G.F.E.D.C.B.A.App' + + ''); + }); + }); describe('ngIf', () => { @@ -304,7 +564,7 @@ describe('@angular/common integration', () => { elementProperty(1, 'ngIf', bind(myApp.showing)); } - function templateOne(rf: RenderFlags, ctx: any) { + function templateOne(rf: RenderFlags, ctx: any, parent: MyApp) { if (rf & RenderFlags.Create) { elementStart(0, 'div'); { text(1); } @@ -314,7 +574,7 @@ describe('@angular/common integration', () => { textBinding(1, bind(myApp.valueOne)); } } - function templateTwo(rf: RenderFlags, ctx: any) { + function templateTwo(rf: RenderFlags, ctx: any, parent: MyApp) { if (rf & RenderFlags.Create) { elementStart(0, 'div'); { text(1); } diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts index 7ade15e91a..5fe7882474 100644 --- a/packages/core/test/render3/component_spec.ts +++ b/packages/core/test/render3/component_spec.ts @@ -7,15 +7,16 @@ */ -import {DoCheck, ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core'; +import {DoCheck, Input, TemplateRef, ViewContainerRef, ViewEncapsulation, createInjector, defineInjectable, defineInjector} from '../../src/core'; import {getRenderedText} from '../../src/render3/component'; -import {ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index'; +import {AttributeMarker, ComponentFactory, LifecycleHooksFeature, defineComponent, directiveInject, markDirty} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding, tick} from '../../src/render3/instructions'; import {ComponentDefInternal, DirectiveDefInternal, RenderFlags} from '../../src/render3/interfaces/definition'; import {createRendererType2} from '../../src/view/index'; +import {NgIf} from './common_with_def'; import {getRendererFactory2} from './imported_renderer2'; -import {ComponentFixture, containerEl, renderComponent, renderToHtml, requestAnimationFrame, toHtml} from './render_util'; +import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame, toHtml} from './render_util'; describe('component', () => { class CounterComponent { @@ -281,8 +282,13 @@ describe('encapsulation', () => { }); describe('recursive components', () => { - let events: string[] = []; - let count = 0; + let events: string[]; + let count: number; + + beforeEach(() => { + events = []; + count = 0; + }); class TreeNode { constructor( @@ -290,11 +296,23 @@ describe('recursive components', () => { public right: TreeNode|null) {} } + /** + * {{ data.value }} + * + * % if (data.left != null) { + * + * % } + * % if (data.right != null) { + * + * % } + */ class TreeComponent { data: TreeNode = _buildTree(0); ngDoCheck() { events.push('check' + this.data.value); } + ngOnDestroy() { events.push('destroy' + this.data.value); } + static ngComponentDef = defineComponent({ type: TreeComponent, selectors: [['tree-comp']], @@ -344,6 +362,58 @@ describe('recursive components', () => { (TreeComponent.ngComponentDef as ComponentDefInternal).directiveDefs = () => [TreeComponent.ngComponentDef]; + /** + * {{ data.value }} + * + * + */ + class NgIfTree { + data: TreeNode = _buildTree(0); + + ngOnDestroy() { events.push('destroy' + this.data.value); } + + static ngComponentDef = defineComponent({ + type: NgIfTree, + selectors: [['ng-if-tree']], + factory: () => new NgIfTree(), + template: (rf: RenderFlags, ctx: NgIfTree) => { + + if (rf & RenderFlags.Create) { + text(0); + container(1, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); + container(2, IfTemplate2, '', [AttributeMarker.SelectOnly, 'ngIf']); + } + if (rf & RenderFlags.Update) { + textBinding(0, bind(ctx.data.value)); + elementProperty(1, 'ngIf', bind(ctx.data.left)); + elementProperty(2, 'ngIf', bind(ctx.data.right)); + } + + function IfTemplate(rf1: RenderFlags, left: any, parent: NgIfTree) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'ng-if-tree'); + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + elementProperty(0, 'data', bind(parent.data.left)); + } + } + function IfTemplate2(rf1: RenderFlags, right: any, parent: NgIfTree) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'ng-if-tree'); + elementEnd(); + } + if (rf1 & RenderFlags.Update) { + elementProperty(0, 'data', bind(parent.data.right)); + } + } + }, + inputs: {data: 'data'}, + }); + } + (NgIfTree.ngComponentDef as ComponentDefInternal).directiveDefs = + () => [NgIfTree.ngComponentDef, NgIf.ngDirectiveDef]; + function _buildTree(currDepth: number): TreeNode { const children = currDepth < 2 ? _buildTree(currDepth + 1) : null; const children2 = currDepth < 2 ? _buildTree(currDepth + 1) : null; @@ -360,6 +430,76 @@ describe('recursive components', () => { expect(events).toEqual(['check6', 'check2', 'check0', 'check1', 'check5', 'check3', 'check4']); }); + // This tests that the view tree is set up properly for recursive components + it('should call onDestroys properly', () => { + + /** + * % if (!skipContent) { + * + * % } + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + container(0); + } + if (rf & RenderFlags.Update) { + containerRefreshStart(0); + if (!ctx.skipContent) { + const rf0 = embeddedViewStart(0); + if (rf0 & RenderFlags.Create) { + elementStart(0, 'tree-comp'); + elementEnd(); + } + embeddedViewEnd(); + } + containerRefreshEnd(); + } + }, [TreeComponent]); + + const fixture = new ComponentFixture(App); + expect(getRenderedText(fixture.component)).toEqual('6201534'); + + events = []; + fixture.component.skipContent = true; + fixture.update(); + expect(events).toEqual( + ['destroy0', 'destroy1', 'destroy2', 'destroy3', 'destroy4', 'destroy5', 'destroy6']); + }); + + it('should call onDestroys properly with ngIf', () => { + /** + * % if (!skipContent) { + * + * % } + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + container(0); + } + if (rf & RenderFlags.Update) { + containerRefreshStart(0); + if (!ctx.skipContent) { + const rf0 = embeddedViewStart(0); + if (rf0 & RenderFlags.Create) { + elementStart(0, 'ng-if-tree'); + elementEnd(); + } + embeddedViewEnd(); + } + containerRefreshEnd(); + } + }, [NgIfTree]); + + const fixture = new ComponentFixture(App); + expect(getRenderedText(fixture.component)).toEqual('6201534'); + + events = []; + fixture.component.skipContent = true; + fixture.update(); + expect(events).toEqual( + ['destroy0', 'destroy1', 'destroy2', 'destroy3', 'destroy4', 'destroy5', 'destroy6']); + }); + it('should map inputs minified & unminified names', async() => { class TestInputsComponent { // TODO(issue/24571): remove '!'. diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index f3ee0e51a0..91eb94c1ec 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -15,6 +15,7 @@ import {AttributeMarker, detectChanges} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; +import {NgIf} from './common_with_def'; import {ComponentFixture, createComponent, renderComponent, toHtml} from './render_util'; describe('content projection', () => { @@ -804,22 +805,6 @@ describe('content projection', () => { }); it('should project into dynamic views (with createEmbeddedView)', () => { - class NgIf { - constructor(public vcr: ViewContainerRef, public template: TemplateRef) {} - - @Input() - set ngIf(value: boolean) { - value ? this.vcr.createEmbeddedView(this.template) : this.vcr.clear(); - } - - static ngDirectiveDef = defineDirective({ - type: NgIf, - selectors: [['', 'ngIf', '']], - inputs: {'ngIf': 'ngIf'}, - factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef()) - }); - } - /** * Before- * @@ -838,7 +823,7 @@ describe('content projection', () => { elementProperty(1, 'ngIf', bind(ctx.showing)); } - function IfTemplate(rf1: RenderFlags, ctx1: any) { + function IfTemplate(rf1: RenderFlags, ctx1: any, child: any) { if (rf1 & RenderFlags.Create) { projectionDef(); projection(0); @@ -884,27 +869,6 @@ describe('content projection', () => { }); it('should project into dynamic views (with insertion)', () => { - class NgIf { - constructor(public vcr: ViewContainerRef, public template: TemplateRef) {} - - @Input() - set ngIf(value: boolean) { - if (value) { - const viewRef = this.template.createEmbeddedView({}); - this.vcr.insert(viewRef); - } else { - this.vcr.clear(); - } - } - - static ngDirectiveDef = defineDirective({ - type: NgIf, - selectors: [['', 'ngIf', '']], - inputs: {'ngIf': 'ngIf'}, - factory: () => new NgIf(injectViewContainerRef(), injectTemplateRef()) - }); - } - /** * Before- * @@ -923,7 +887,7 @@ describe('content projection', () => { elementProperty(1, 'ngIf', bind(ctx.showing)); } - function IfTemplate(rf1: RenderFlags, ctx1: any) { + function IfTemplate(rf1: RenderFlags, ctx1: any, child: any) { if (rf1 & RenderFlags.Create) { projectionDef(); projection(0); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 9923a2d1b7..bf37e84852 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -8,11 +8,12 @@ import {Component, ComponentFactoryResolver, Directive, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core'; import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; -import {NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, reserveSlots, text, textBinding} from '../../src/render3/instructions'; +import {AttributeMarker,NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; +import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, load, loadDirective, projection, projectionDef, reserveSlots, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgModuleFactory} from '../../src/render3/ng_module_ref'; import {pipe, pipeBind1} from '../../src/render3/pipe'; +import {NgForOf} from '../../test/render3/common_with_def'; import {getRendererFactory2} from './imported_renderer2'; import {ComponentFixture, TemplateFixture, createComponent} from './render_util'; @@ -447,6 +448,224 @@ describe('ViewContainerRef', () => { }); }); + describe('insertion points and declaration points', () => { + class InsertionDir { + // @Input() + set tplDir(tpl: TemplateRef|null) { + tpl ? this.vcr.createEmbeddedView(tpl) : this.vcr.clear(); + } + + constructor(public vcr: ViewContainerRef) {} + + static ngDirectiveDef = defineDirective({ + type: InsertionDir, + selectors: [['', 'tplDir', '']], + factory: () => new InsertionDir(injectViewContainerRef()), + inputs: {tplDir: 'tplDir'} + }); + } + + // see running stackblitz example: https://stackblitz.com/edit/angular-w3myy6 + it('should work with a template declared in a different component view from insertion', + () => { + let child: Child|null = null; + + /** + *
    {{ name }}
    + * // template insertion point + */ + class Child { + name = 'Child'; + tpl: TemplateRef|null = null; + + static ngComponentDef = defineComponent({ + type: Child, + selectors: [['child']], + factory: () => child = new Child(), + template: function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div', [AttributeMarker.SelectOnly, 'tplDir']); + { text(1); } + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'tplDir', bind(ctx.tpl)); + textBinding(1, bind(ctx.name)); + } + }, + inputs: {tpl: 'tpl'}, + directives: () => [InsertionDir] + }); + } + + /** + * // template declaration point + * + *
    {{ name }}
    + *
    + * + * <-- template insertion inside + */ + const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + container(0, template); + elementStart(1, 'child'); + elementEnd(); + } + + if (rf & RenderFlags.Update) { + // Hack until we have local refs for templates + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(1, 'tpl', bind(tplRef)); + } + + function template(rf1: RenderFlags, ctx1: any, parent: any) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'div'); + { text(1); } + elementEnd(); + } + + if (rf1 & RenderFlags.Update) { + textBinding(1, bind(parent.name)); + } + } + }, [Child]); + + const fixture = new ComponentFixture(Parent); + fixture.component.name = 'Parent'; + fixture.update(); + + // Context should be inherited from the declaration point, not the insertion point, + // so the template should read 'Parent'. + expect(fixture.html).toEqual(`
    Child
    Parent
    `); + + child !.tpl = null; + fixture.update(); + expect(fixture.html).toEqual(`
    Child
    `); + }); + + // see running stackblitz example: https://stackblitz.com/edit/angular-3vplec + it('should work with nested for loops with different declaration / insertion points', () => { + /** + * + * // insertion point for templates (both row and cell) + * + */ + class LoopComp { + name = 'Loop'; + + // @Input() + tpl !: TemplateRef; + + // @Input() + rows !: any[]; + + static ngComponentDef = defineComponent({ + type: LoopComp, + selectors: [['loop-comp']], + factory: () => new LoopComp(), + template: function(rf: RenderFlags, loop: any) { + if (rf & RenderFlags.Create) { + container(0, () => {}, null, [AttributeMarker.SelectOnly, 'ngForOf']); + } + + if (rf & RenderFlags.Update) { + elementProperty(0, 'ngForOf', bind(loop.rows)); + elementProperty(0, 'ngForTemplate', bind(loop.tpl)); + } + }, + inputs: {tpl: 'tpl', rows: 'rows'}, + directives: () => [NgForOf] + }); + } + + /** + * // row declaration point + * + * + * // cell declaration point + * + *
    {{ cell }} - {{ row.value }} - {{ name }}
    + *
    + * + * <-- cell insertion + *
    + * + * <-- row insertion + * + */ + const Parent = createComponent('parent', function(rf: RenderFlags, parent: any) { + if (rf & RenderFlags.Create) { + container(0, rowTemplate); + elementStart(1, 'loop-comp'); + elementEnd(); + } + + if (rf & RenderFlags.Update) { + // Hack until we have local refs for templates + const rowTemplateRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(1, 'tpl', bind(rowTemplateRef)); + elementProperty(1, 'rows', bind(parent.rows)); + } + + function rowTemplate(rf1: RenderFlags, row: any, parent: any) { + if (rf1 & RenderFlags.Create) { + container(0, cellTemplate); + elementStart(1, 'loop-comp'); + elementEnd(); + } + + if (rf1 & RenderFlags.Update) { + // Hack until we have local refs for templates + const cellTemplateRef = + getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(1, 'tpl', bind(cellTemplateRef)); + elementProperty(1, 'rows', bind(row.$implicit.data)); + } + } + + function cellTemplate(rf1: RenderFlags, cell: any, row: any, parent: any) { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'div'); + { text(1); } + elementEnd(); + } + + if (rf1 & RenderFlags.Update) { + textBinding( + 1, interpolation3( + '', cell.$implicit, ' - ', row.$implicit.value, ' - ', parent.name, '')); + } + } + }, [LoopComp]); + + const fixture = new ComponentFixture(Parent); + fixture.component.name = 'Parent'; + fixture.component.rows = + [{data: ['1', '2'], value: 'one'}, {data: ['3', '4'], value: 'two'}]; + fixture.update(); + + expect(fixture.html) + .toEqual( + '' + + '
    1 - one - Parent
    2 - one - Parent
    ' + + '
    3 - two - Parent
    4 - two - Parent
    ' + + '
    '); + + fixture.component.rows = [{data: ['5', '6'], value: 'three'}, {data: ['7'], value: 'four'}]; + fixture.component.name = 'New name!'; + fixture.update(); + + expect(fixture.html) + .toEqual( + '' + + '
    5 - three - New name!
    6 - three - New name!
    ' + + '
    7 - four - New name!
    ' + + '
    '); + }); + }); + const rendererFactory = getRendererFactory2(document); describe('detach', () => {