fix(ivy): nested ngFor should be supported (#24564)

PR Close #24564
This commit is contained in:
Marc Laval 2018-06-18 18:07:05 +02:00 committed by Miško Hevery
parent a26965812b
commit 8b8168262d
5 changed files with 94 additions and 3 deletions

View File

@ -590,6 +590,7 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer
lContainerNode.tNode = hostTNode.dynamicContainerNode; lContainerNode.tNode = hostTNode.dynamicContainerNode;
vcRefHost.dynamicLContainerNode = lContainerNode; vcRefHost.dynamicLContainerNode = lContainerNode;
lContainerNode.dynamicParent = vcRefHost;
addToViewTree(vcRefHost.view, hostTNode.index as number, lContainer); addToViewTree(vcRefHost.view, hostTNode.index as number, lContainer);
@ -649,6 +650,7 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
(viewRef as EmbeddedViewRef<any>).attachToViewContainerRef(this); (viewRef as EmbeddedViewRef<any>).attachToViewContainerRef(this);
insertView(this._lContainerNode, lViewNode, adjustedIdx); insertView(this._lContainerNode, lViewNode, adjustedIdx);
lViewNode.dynamicParent = this._lContainerNode;
this._viewRefs.splice(adjustedIdx, 0, viewRef); this._viewRefs.splice(adjustedIdx, 0, viewRef);
@ -672,7 +674,8 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
detach(index?: number): viewEngine_ViewRef|null { detach(index?: number): viewEngine_ViewRef|null {
const adjustedIdx = this._adjustIndex(index, -1); const adjustedIdx = this._adjustIndex(index, -1);
detachView(this._lContainerNode, adjustedIdx); const lViewNode = detachView(this._lContainerNode, adjustedIdx);
lViewNode.dynamicParent = null;
return this._viewRefs.splice(adjustedIdx, 1)[0] || null; return this._viewRefs.splice(adjustedIdx, 1)[0] || null;
} }

View File

@ -318,7 +318,8 @@ export function createLNodeObject(
queries: queries, queries: queries,
tNode: null !, tNode: null !,
pNextOrParent: null, pNextOrParent: null,
dynamicLContainerNode: null dynamicLContainerNode: null,
dynamicParent: null
}; };
} }

View File

@ -111,6 +111,12 @@ export interface LNode {
*/ */
// TODO(kara): Remove when removing LNodes // TODO(kara): Remove when removing LNodes
dynamicLContainerNode: LContainerNode|null; dynamicLContainerNode: LContainerNode|null;
/**
* A pointer to a parent LNode created dynamically and virtually by directives requesting
* ViewContainerRef. Applicable only to LContainerNode and LViewNode.
*/
dynamicParent: LElementNode|LContainerNode|LViewNode|null;
} }
@ -129,6 +135,7 @@ export interface LTextNode extends LNode {
native: RText; native: RText;
readonly data: null; readonly data: null;
dynamicLContainerNode: null; dynamicLContainerNode: null;
dynamicParent: null;
} }
/** Abstract node which contains root nodes of a view. */ /** Abstract node which contains root nodes of a view. */

View File

@ -42,7 +42,10 @@ export function getParentLNode(node: LContainerNode | LElementNode | LTextNode |
export function getParentLNode(node: LViewNode): LContainerNode|null; export function getParentLNode(node: LViewNode): LContainerNode|null;
export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNode|null; export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNode|null;
export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNode|null { export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNode|null {
if (node.tNode.index === -1) return null; if (node.tNode.index === -1) {
// This is a dynamic container or an embedded view inside a dynamic container.
return node.dynamicParent;
}
const parent = node.tNode.parent; const parent = node.tNode.parent;
return parent ? node.view[parent.index] : node.view[HOST_NODE]; return parent ? node.view[parent.index] : node.view[HOST_NODE];
} }

View File

@ -199,6 +199,83 @@ describe('@angular/common integration', () => {
expect(fixture.html) expect(fixture.html)
.toEqual('<button>Toggle List</button><ul><li>1</li><li>2</li><li>3</li></ul>'); .toEqual('<button>Toggle List</button><ul><li>1</li><li>2</li><li>3</li></ul>');
}); });
it('should support multiple levels of embedded templates', () => {
/**
* <ul *ngFor="let outterItem of items.">
* <li *ngFor="let item of items">
* <span>{{item}}</span>
* </li>
* </ul>
*/
class MyApp {
items: string[] = ['1', '2'];
static ngComponentDef = defineComponent({
type: MyApp,
factory: () => new MyApp(),
selectors: [['my-app']],
template: (rf: RenderFlags, myApp: MyApp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'ul');
{ container(1, liTemplate, null, ['ngForOf', '']); }
elementEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(1, 'ngForOf', bind(myApp.items));
}
function liTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'li');
{ container(1, spanTemplate, null, ['ngForOf', '']); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
elementProperty(1, 'ngForOf', bind(myApp.items));
}
}
function spanTemplate(rf1: RenderFlags, row: NgForOfContext<string>) {
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
{ text(1); }
elementEnd();
}
if (rf1 & RenderFlags.Update) {
textBinding(1, bind(row.$implicit));
}
}
},
directives: () => [NgForOf]
});
}
const fixture = new ComponentFixture(MyApp);
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html)
.toEqual(
'<ul><li><span>1</span><span>2</span></li><li><span>1</span><span>2</span></li></ul>');
// Remove the last item
fixture.component.items.length = 1;
fixture.update();
expect(fixture.html).toEqual('<ul><li><span>1</span></li></ul>');
// Change an item
fixture.component.items[0] = 'one';
fixture.update();
expect(fixture.html).toEqual('<ul><li><span>one</span></li></ul>');
// Add an item
fixture.component.items.push('two');
fixture.update();
expect(fixture.html)
.toEqual(
'<ul><li><span>one</span><span>two</span></li><li><span>one</span><span>two</span></li></ul>');
});
}); });
describe('ngIf', () => { describe('ngIf', () => {