diff --git a/packages/common/test/directives/ng_switch_spec.ts b/packages/common/test/directives/ng_switch_spec.ts
index 9d78c6f6d9..f7042859e5 100644
--- a/packages/common/test/directives/ng_switch_spec.ts
+++ b/packages/common/test/directives/ng_switch_spec.ts
@@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
-import {Attribute, Component, Directive} from '@angular/core';
+import {Attribute, Component, Directive, TemplateRef, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -26,7 +26,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
beforeEach(() => {
TestBed.configureTestingModule({
- declarations: [TestComponent],
+ declarations: [TestComponent, ComplexComponent],
imports: [CommonModule],
});
});
@@ -171,6 +171,20 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
getComponent().switchValue = 'b';
detectChangesAndExpectText('when b1;when b2;');
});
+
+ it('should support nested NgSwitch on ng-container with ngTemplateOutlet', () => {
+ fixture = TestBed.createComponent(ComplexComponent);
+ detectChangesAndExpectText('Foo');
+
+ fixture.componentInstance.state = 'case2';
+ detectChangesAndExpectText('Bar');
+
+ fixture.componentInstance.state = 'notACase';
+ detectChangesAndExpectText('Default');
+
+ fixture.componentInstance.state = 'case1';
+ detectChangesAndExpectText('Foo');
+ });
});
});
}
@@ -182,6 +196,38 @@ class TestComponent {
when2: any = null;
}
+@Component({
+ selector: 'complex-cmp',
+ template: `
+
+
+
+ Should never render
+
+
+
+ Should never render
+
+
+
+ Default
+
+
+
+
+ Foo
+
+
+ Bar
+
+`
+})
+class ComplexComponent {
+ @ViewChild('foo') foo !: TemplateRef;
+ @ViewChild('bar') bar !: TemplateRef;
+ state: string = 'case1';
+}
+
function createTestComponent(template: string): ComponentFixture {
return TestBed.overrideComponent(TestComponent, {set: {template: template}})
.createComponent(TestComponent);
diff --git a/packages/core/src/render3/instructions/instructions.ts b/packages/core/src/render3/instructions/instructions.ts
index e3c3e78934..a5e32252b6 100644
--- a/packages/core/src/render3/instructions/instructions.ts
+++ b/packages/core/src/render3/instructions/instructions.ts
@@ -1952,7 +1952,7 @@ function generateInitialInputs(
*/
export function createLContainer(
hostNative: RElement | RComment | StylingContext | LView, currentView: LView, native: RComment,
- isForViewContainerRef?: boolean): LContainer {
+ tNode: TNode, isForViewContainerRef?: boolean): LContainer {
ngDevMode && assertDomNode(native);
ngDevMode && assertLView(currentView);
const lContainer: LContainer = [
@@ -1962,8 +1962,9 @@ export function createLContainer(
currentView, // parent
null, // next
null, // queries
- [], // views
+ tNode, // t_host
native, // native
+ [], // views
];
ngDevMode && attachLContainerDebug(lContainer);
return lContainer;
@@ -2037,7 +2038,8 @@ function containerInternal(
const comment = lView[RENDERER].createComment(ngDevMode ? 'container' : '');
ngDevMode && ngDevMode.rendererCreateComment++;
const tNode = createNodeAtIndex(index, TNodeType.Container, comment, tagName, attrs);
- const lContainer = lView[adjustedIndex] = createLContainer(lView[adjustedIndex], lView, comment);
+ const lContainer = lView[adjustedIndex] =
+ createLContainer(lView[adjustedIndex], lView, comment, tNode);
appendChild(comment, tNode, lView);
diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts
index c562cab613..3d1a61a967 100644
--- a/packages/core/src/render3/interfaces/container.ts
+++ b/packages/core/src/render3/interfaces/container.ts
@@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {TNode} from './node';
import {LQueries} from './query';
import {RComment, RElement} from './renderer';
import {StylingContext} from './styling';
-import {HOST, LView, NEXT, PARENT, QUERIES} from './view';
+import {HOST, LView, NEXT, PARENT, QUERIES, T_HOST} from './view';
+
/**
* Special location which allows easy identification of type. If we have an array which was
@@ -23,10 +25,10 @@ export const TYPE = 1;
* Uglify will inline these when minifying so there shouldn't be a cost.
*/
export const ACTIVE_INDEX = 2;
-// PARENT, NEXT, and QUERIES are indices 3, 4, and 5.
+// PARENT, NEXT, QUERIES and T_HOST are indices 3, 4, 5 and 6.
// As we already have these constants in LView, we don't need to re-create them.
-export const VIEWS = 6;
export const NATIVE = 7;
+export const VIEWS = 8;
/**
* The state associated with a container.
@@ -83,17 +85,22 @@ export interface LContainer extends Array {
// `[QUERIES]` in it which are not needed for `LContainer` (only needed for Template)
/**
- * A list of the container's currently active child views. Views will be inserted
- * here as they are added and spliced from here when they are removed. We need
- * to keep a record of current views so we know which views are already in the DOM
- * (and don't need to be re-added) and so we can remove views from the DOM when they
- * are no longer required.
+ * Pointer to the `TNode` which represents the host of the container.
*/
- [VIEWS]: LView[];
+ [T_HOST]: TNode;
/** The comment element that serves as an anchor for this LContainer. */
readonly[NATIVE]:
RComment; // TODO(misko): remove as this value can be gotten by unwrapping `[HOST]`
+
+ /**
+*A list of the container's currently active child views. Views will be inserted
+*here as they are added and spliced from here when they are removed. We need
+*to keep a record of current views so we know which views are already in the DOM
+*(and don't need to be re-added) and so we can remove views from the DOM when they
+*are no longer required.
+*/
+ [VIEWS]: LView[];
}
// Note: This hack is necessary so we don't erroneously get a circular dependency
diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts
index 717fa06d73..14ad1e7541 100644
--- a/packages/core/src/render3/node_manipulation.ts
+++ b/packages/core/src/render3/node_manipulation.ts
@@ -91,7 +91,7 @@ function walkTNodeTree(
let tNode: TNode|null = rootTNode.child as TNode;
while (tNode) {
let nextTNode: TNode|null = null;
- if (tNode.type === TNodeType.Element) {
+ if (tNode.type === TNodeType.Element || tNode.type === TNodeType.ElementContainer) {
executeNodeAction(
action, renderer, renderParent, getNativeByTNode(tNode, currentView), tNode, beforeNode);
const nodeOrContainer = currentView[tNode.index];
@@ -99,6 +99,14 @@ function walkTNodeTree(
// This element has an LContainer, and its comment needs to be handled
executeNodeAction(
action, renderer, renderParent, nodeOrContainer[NATIVE], tNode, beforeNode);
+ if (nodeOrContainer[VIEWS].length) {
+ currentView = nodeOrContainer[VIEWS][0];
+ nextTNode = currentView[TVIEW].node;
+
+ // When the walker enters a container, then the beforeNode has to become the local native
+ // comment node.
+ beforeNode = nodeOrContainer[NATIVE];
+ }
}
} else if (tNode.type === TNodeType.Container) {
const lContainer = currentView ![tNode.index] as LContainer;
@@ -133,9 +141,8 @@ function walkTNodeTree(
nextTNode = currentView[TVIEW].data[head.index] as TNode;
}
}
-
} else {
- // Otherwise, this is a View or an ElementContainer
+ // Otherwise, this is a View
nextTNode = tNode.child;
}
@@ -145,7 +152,14 @@ function walkTNodeTree(
currentView = projectionNodeStack[projectionNodeIndex--] as LView;
tNode = projectionNodeStack[projectionNodeIndex--] as TNode;
}
- nextTNode = (tNode.flags & TNodeFlags.isProjected) ? tNode.projectionNext : tNode.next;
+
+ if (tNode.flags & TNodeFlags.isProjected) {
+ nextTNode = tNode.projectionNext;
+ } else if (tNode.type === TNodeType.ElementContainer) {
+ nextTNode = tNode.child || tNode.next;
+ } else {
+ nextTNode = tNode.next;
+ }
/**
* Find the next node in the TNode tree, taking into account the place where a node is
@@ -172,19 +186,26 @@ function walkTNodeTree(
* chain until:
* - we find an lView with a next pointer
* - or find a tNode with a parent that has a next pointer
+ * - or find a lContainer
* - or reach root TNode (in which case we exit, since we traversed all nodes)
*/
while (!currentView[NEXT] && currentView[PARENT] &&
!(tNode.parent && tNode.parent.next)) {
if (tNode === rootTNode) return;
currentView = currentView[PARENT] as LView;
+ if (isLContainer(currentView)) {
+ tNode = currentView[T_HOST] !;
+ currentView = currentView[PARENT];
+ beforeNode = currentView[tNode.index][NATIVE];
+ break;
+ }
tNode = currentView[T_HOST] !;
}
if (currentView[NEXT]) {
currentView = currentView[NEXT] as LView;
nextTNode = currentView[T_HOST];
} else {
- nextTNode = tNode.next;
+ nextTNode = tNode.type === TNodeType.ElementContainer && tNode.child || tNode.next;
}
} else {
nextTNode = tNode.next;
diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts
index 7bba5c33d7..67c544a58c 100644
--- a/packages/core/src/render3/view_engine_compatibility.ts
+++ b/packages/core/src/render3/view_engine_compatibility.ts
@@ -321,7 +321,7 @@ export function createContainerRef(
}
hostView[hostTNode.index] = lContainer =
- createLContainer(slotValue, hostView, commentNode, true);
+ createLContainer(slotValue, hostView, commentNode, hostTNode, true);
addToViewTree(hostView, lContainer);
}
diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts
index cc5b963612..1e921e55fc 100644
--- a/packages/core/test/acceptance/view_container_ref_spec.ts
+++ b/packages/core/test/acceptance/view_container_ref_spec.ts
@@ -6,15 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Component, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
+import {Component, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
-import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
+import {ivyEnabled, onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing';
describe('ViewContainerRef', () => {
+ const TRANSLATIONS: any = {
+ 'Bar': 'o',
+ '{$startTagBefore}{$closeTagBefore}{$startTagDiv}{$startTagInside}{$closeTagInside}{$closeTagDiv}{$startTagAfter}{$closeTagAfter}':
+ 'F{$startTagDiv}{$closeTagDiv}o',
+ '{$startTagBefore}{$closeTagBefore}{$startTagDiv}{$startTagIn}{$closeTagIn}{$closeTagDiv}{$startTagAfter}{$closeTagAfter}':
+ '{$startTagDiv}{$closeTagDiv}{$startTagBefore}{$closeTagBefore}'
+ };
+
beforeEach(() => {
- TestBed.configureTestingModule({declarations: [ViewContainerRefComp, ViewContainerRefApp]});
+ polyfillGoogGetMsg(TRANSLATIONS);
+ TestBed.configureTestingModule(
+ {declarations: [StructDir, ViewContainerRefComp, ViewContainerRefApp, DestroyCasesComp]});
});
describe('insert', () => {
@@ -80,6 +90,142 @@ describe('ViewContainerRef', () => {
});
});
+ describe('destroy should clean the DOM in all cases:', () => {
+ function executeTest(template: string) {
+ TestBed.overrideTemplate(DestroyCasesComp, template).configureTestingModule({
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+
+ const fixture = TestBed.createComponent(DestroyCasesComp);
+ fixture.detectChanges();
+ const initial = fixture.nativeElement.innerHTML;
+
+ const structDirs = fixture.componentInstance.structDirs.toArray();
+
+ structDirs.forEach(structDir => structDir.create());
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveText('Foo');
+
+ structDirs.forEach(structDir => structDir.destroy());
+ fixture.detectChanges();
+ expect(fixture.nativeElement.innerHTML).toEqual(initial);
+ }
+
+ it('when nested ng-container', () => {
+ executeTest(`
+
+
+
+
+
+ Foo
+
+
+
+
+ `);
+ });
+
+ it('when ViewContainerRef is on a ng-container', () => {
+ executeTest(`
+
+ Foo
+
+
+
+
+
+
+
+
+ `);
+ });
+
+ it('when ViewContainerRef is on an element', () => {
+ executeTest(`
+
+ Foo
+
+
+
+
+
+
+
+
+ `);
+ });
+
+ it('when ViewContainerRef is on a ng-template', () => {
+ executeTest(`
+
+ Foo
+
+
+
+
+
+
+ `);
+ });
+
+ it('when ViewContainerRef is on an element inside a ng-container', () => {
+ executeTest(`
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+ });
+
+ onlyInIvy('Ivy i18n logic')
+ .it('when ViewContainerRef is on an element inside a ng-container with i18n', () => {
+ executeTest(`
+
+ Bar
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+ });
+
+ onlyInIvy('Ivy i18n logic')
+ .it('when ViewContainerRef is on an element, and i18n is on the parent ViewContainerRef',
+ () => {
+ executeTest(`
+
+ Foo
+
+
+
+
+
+
+
+
+ `);
+ });
+ });
+
});
@Component({
@@ -105,3 +251,17 @@ class ViewContainerRefComp {
class ViewContainerRefApp {
@ViewChild(ViewContainerRefComp) vcrComp !: ViewContainerRefComp;
}
+
+@Directive({selector: '[structDir]'})
+export class StructDir {
+ constructor(private vcref: ViewContainerRef, private tplRef: TemplateRef) {}
+
+ create() { this.vcref.createEmbeddedView(this.tplRef); }
+
+ destroy() { this.vcref.clear(); }
+}
+
+@Component({selector: 'destroy-cases', template: ` `})
+class DestroyCasesComp {
+ @ViewChildren(StructDir) structDirs !: QueryList;
+}
diff --git a/packages/core/test/render3/view_utils_spec.ts b/packages/core/test/render3/view_utils_spec.ts
index e8ea579dcf..1603faad8d 100644
--- a/packages/core/test/render3/view_utils_spec.ts
+++ b/packages/core/test/render3/view_utils_spec.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {createLContainer, createLView, createTView} from '@angular/core/src/render3/instructions/all';
+import {createLContainer, createLView, createTNode, createTView} from '@angular/core/src/render3/instructions/all';
import {createEmptyStylingContext} from '@angular/core/src/render3/styling/util';
import {isLContainer, isLView, isStylingContext, unwrapLContainer, unwrapLView, unwrapRNode, unwrapStylingContext} from '@angular/core/src/render3/util/view_utils';
@@ -15,7 +15,8 @@ describe('view_utils', () => {
const div = document.createElement('div');
const tView = createTView(0, null, 0, 0, null, null, null, null);
const lView = createLView(null, tView, {}, 0, div, null, {} as any, {} as any, null, null);
- const lContainer = createLContainer(lView, lView, div, true);
+ const tNode = createTNode(null, 3, 0, 'div', []);
+ const lContainer = createLContainer(lView, lView, div, tNode, true);
const styleContext = createEmptyStylingContext(lContainer, null, null, null);
expect(unwrapRNode(styleContext)).toBe(div);
diff --git a/packages/private/testing/src/goog_get_msg.ts b/packages/private/testing/src/goog_get_msg.ts
index 2547839cd8..829dcb70ca 100644
--- a/packages/private/testing/src/goog_get_msg.ts
+++ b/packages/private/testing/src/goog_get_msg.ts
@@ -17,7 +17,8 @@ export function polyfillGoogGetMsg(translations: {[key: string]: string} = {}):
const glob = (global as any);
glob.goog = glob.goog || {};
glob.goog.getMsg = function(input: string, placeholders: {[key: string]: string} = {}) {
- if (typeof translations[input] !== 'undefined') { // to account for empty string
+ if (typeof translations[input] !== 'undefined') { // to account for
+ // empty string
input = translations[input];
}
return Object.keys(placeholders).length ?