From 65544ac7427af2c6866f01e92a10eb8236daa3b4 Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Tue, 11 Jun 2019 19:01:49 +0200 Subject: [PATCH] fix(ivy): reprojected ICU expression nodes when creating embedded views (#30979) When using `createEmbeddedView` after the creation of an ICU expression, the nodes for the current selected case were not reprojected (only the anchor comment node was moved to the new location). Now we reproject correctly all the child nodes of an ICU expression when an anchor comment node is projected. FW-1372 #resolve PR Close #30979 --- .../core/src/render3/node_manipulation.ts | 30 ++++- packages/core/test/acceptance/i18n_spec.ts | 111 ++++++++++++++++++ 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 50dd8cd1d9..5d78c9c8d2 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -7,7 +7,6 @@ */ import {ViewEncapsulation} from '../metadata/view'; - import {assertLContainer, assertLView} from './assert'; import {attachPatchData} from './context_discovery'; import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; @@ -801,6 +800,23 @@ export function appendProjectedNodes( } } +/** + * Loops over all children of a TNode container and appends them to the DOM + * + * @param ngContainerChildTNode The first child of the TNode container + * @param tProjectionNode The projection (ng-content) TNode + * @param currentView Current LView + * @param projectionView Projection view (view above current) + */ +function appendProjectedChildren( + ngContainerChildTNode: TNode | null, tProjectionNode: TNode, currentView: LView, + projectionView: LView) { + while (ngContainerChildTNode) { + appendProjectedNode(ngContainerChildTNode, tProjectionNode, currentView, projectionView); + ngContainerChildTNode = ngContainerChildTNode.next; + } +} + /** * Appends a projected node to the DOM, or in the case of a projected container, * appends the nodes from all of the container's active views to the DOM. @@ -831,13 +847,15 @@ function appendProjectedNode( for (let i = CONTAINER_HEADER_OFFSET; i < nodeOrContainer.length; i++) { addRemoveViewFromContainer(nodeOrContainer[i], true, nodeOrContainer[NATIVE]); } + } else if (projectedTNode.type === TNodeType.IcuContainer) { + // The node we are adding is an ICU container which is why we also need to project all the + // children nodes that might have been created previously and are linked to this anchor + let ngContainerChildTNode: TNode|null = projectedTNode.child as TNode; + appendProjectedChildren( + ngContainerChildTNode, ngContainerChildTNode, projectionView, projectionView); } else { if (projectedTNode.type === TNodeType.ElementContainer) { - let ngContainerChildTNode: TNode|null = projectedTNode.child as TNode; - while (ngContainerChildTNode) { - appendProjectedNode(ngContainerChildTNode, tProjectionNode, currentView, projectionView); - ngContainerChildTNode = ngContainerChildTNode.next; - } + appendProjectedChildren(projectedTNode.child, tProjectionNode, currentView, projectionView); } if (isLContainer(nodeOrContainer)) { diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 10eb551258..ef4c5c86dc 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -661,6 +661,117 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { const element = fixture.nativeElement; expect(element).toHaveText('other'); }); + + it('inside a container when creating a view via vcr.createEmbeddedView', () => { + @Directive({ + selector: '[someDir]', + }) + class Dir { + constructor( + private readonly viewContainerRef: ViewContainerRef, + private readonly templateRef: TemplateRef) {} + + ngOnInit() { this.viewContainerRef.createEmbeddedView(this.templateRef); } + } + + @Component({ + selector: 'my-cmp', + template: ` +
+ +
+ `, + }) + class Cmp { + } + + @Component({ + selector: 'my-app', + template: ` + { + count, + plural, + =1 {ONE} + other {OTHER} + } + `, + }) + class App { + count = 1; + } + + TestBed.configureTestingModule({ + declarations: [App, Cmp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML) + .toBe('
ONE
'); + + fixture.componentRef.instance.count = 2; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML) + .toBe('
OTHER
'); + }); + + it('with nested ICU expression and inside a container when creating a view via vcr.createEmbeddedView', + () => { + @Directive({ + selector: '[someDir]', + }) + class Dir { + constructor( + private readonly viewContainerRef: ViewContainerRef, + private readonly templateRef: TemplateRef) {} + + ngOnInit() { this.viewContainerRef.createEmbeddedView(this.templateRef); } + } + + @Component({ + selector: 'my-cmp', + template: ` +
+ +
+ `, + }) + class Cmp { + } + + @Component({ + selector: 'my-app', + template: ` + { + count, + plural, + =1 {ONE} + other {{{count}} {name, select, + cat {cats} + dog {dogs} + other {animals} + }!} + } + `, + }) + class App { + count = 1; + } + + TestBed.configureTestingModule({ + declarations: [App, Cmp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.componentRef.instance.count = 2; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML) + .toBe( + '
2 animals!
'); + + fixture.componentRef.instance.count = 1; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.innerHTML) + .toBe('
ONE
'); + }); }); describe('should support attributes', () => {