Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ivy): <ng-container> fixes #25617

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/core/src/render3/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,17 +557,21 @@ export const QUERY_READ_ELEMENT_REF =

export const QUERY_READ_FROM_NODE =
(new ReadFromInjectorFn<any>((injector: LInjector, node: LNode, directiveIdx: number) => {
ngDevMode && assertNodeOfPossibleTypes(node, TNodeType.Container, TNodeType.Element);
ngDevMode && assertNodeOfPossibleTypes(
node, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
if (directiveIdx > -1) {
return node.view[DIRECTIVES] ![directiveIdx];
}
if (node.tNode.type === TNodeType.Element) {
if (node.tNode.type === TNodeType.Element || node.tNode.type === TNodeType.ElementContainer) {
return getOrCreateElementRef(injector);
}
if (node.tNode.type === TNodeType.Container) {
return getOrCreateTemplateRef(injector);
}
throw new Error('fail');
if (ngDevMode) {
// should never happen
throw new Error(`Unexpected node type: ${node.tNode.type}`);
}
}) as any as QueryReadType<any>);

/** A ref to a node's native element. */
Expand All @@ -586,7 +590,8 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer
if (!di.viewContainerRef) {
const vcRefHost = di.node;

ngDevMode && assertNodeOfPossibleTypes(vcRefHost, TNodeType.Container, TNodeType.Element);
ngDevMode && assertNodeOfPossibleTypes(
vcRefHost, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
const hostParent = getParentLNode(vcRefHost) !;
const lContainer = createLContainer(hostParent, vcRefHost.view, true);
const comment = vcRefHost.view[RENDERER].createComment(ngDevMode ? 'container' : '');
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/render3/node_assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ export function assertNodeType(node: LNode, type: TNodeType) {
export function assertNodeOfPossibleTypes(node: LNode, ...types: TNodeType[]) {
assertDefined(node, 'should be called with a node');
const found = types.some(type => node.tNode.type === type);
assertEqual(found, true, `Should be one of ${types.map(typeName).join(', ')}`);
assertEqual(
found, true,
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(node.tNode.type)}`);
}

function typeName(type: TNodeType): string {
if (type == TNodeType.Projection) return 'Projection';
if (type == TNodeType.Container) return 'Container';
if (type == TNodeType.View) return 'View';
if (type == TNodeType.Element) return 'Element';
if (type == TNodeType.ElementContainer) return 'ElementContainer';
return '<unknown>';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be more performant? j/w

// Move this object outside so it only gets created once
const nodeTypeName = {
  TNodeType.Projection: 'Projection',
  TNodeType.Container: 'Container',
  TNodeType.View: 'View',
  TNodeType.Element: 'Element',
  TNodeType.ElementContainer: 'ElementContainer',
};

return nodeTypeName[type] || '<unknown>';

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that performance is consideration here since this is just assert code not present in production bundles.

}
47 changes: 46 additions & 1 deletion packages/core/test/render3/common_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {NgForOfContext} from '@angular/common';

import {AttributeMarker, defineComponent, templateRefExtractor} from '../../src/render3/index';
import {bind, template, elementEnd, elementProperty, elementStart, getCurrentView, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, nextContext, restoreView, text, textBinding} from '../../src/render3/instructions';
import {bind, template, elementEnd, elementProperty, elementStart, getCurrentView, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, nextContext, restoreView, text, textBinding, elementContainerStart, elementContainerEnd, reference} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';

import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def';
Expand Down Expand Up @@ -924,5 +924,50 @@ describe('@angular/common integration', () => {
expect(fixture.html).toEqual('');
});

it('should allow usage on ng-container', () => {
class MyApp {
showing = false;
static ngComponentDef = defineComponent({
type: MyApp,
factory: () => new MyApp(),
selectors: [['my-app']],
consts: 3,
vars: 1,
/**
* <ng-template #tpl>from tpl</ng-template>
* <ng-container [ngTemplateOutlet]="showing ? tpl : null"></ng-container>
*/
template: (rf: RenderFlags, myApp: MyApp) => {
if (rf & RenderFlags.Create) {
template(0, (rf1: RenderFlags) => {
if (rf1 & RenderFlags.Create) {
text(0, 'from tpl');
}
}, 1, 0, undefined, undefined, ['tpl', ''], templateRefExtractor);
elementContainerStart(2, [AttributeMarker.SelectOnly, 'ngTemplateOutlet']);
elementContainerEnd();
}
if (rf & RenderFlags.Update) {
const tplRef = reference(1);
elementProperty(2, 'ngTemplateOutlet', bind(myApp.showing ? tplRef : null));
}
},
directives: () => [NgTemplateOutlet]
});
}

const fixture = new ComponentFixture(MyApp);
expect(fixture.html).toEqual('');

fixture.component.showing = true;
fixture.update();
expect(fixture.html).toEqual('from tpl');

fixture.component.showing = false;
fixture.update();
expect(fixture.html).toEqual('');

});

});
});
37 changes: 36 additions & 1 deletion packages/core/test/render3/query_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ describe('query', () => {
/**
* <ng-container #foo></ng-container>
* class Cmpt {
* @ViewChildren('foo') query;
* @ViewChildren('foo', {read: ElementRef}) query;
* }
*/
const Cmpt = createComponent(
Expand Down Expand Up @@ -401,6 +401,41 @@ describe('query', () => {
expect(qList.first.nativeElement).toEqual(elToQuery);
});

it('should query for <ng-container> and read ElementRef without explicit read option', () => {
let elToQuery;
/**
* <ng-container #foo></ng-container>
* class Cmpt {
* @ViewChildren('foo') query;
* }
*/
const Cmpt = createComponent(
'cmpt',
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementContainerStart(1, null, ['foo', '']);
elToQuery = loadElement(1).native;
elementContainerEnd();
}
},
3, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
query(0, ['foo'], true, QUERY_READ_FROM_NODE);
}
if (rf & RenderFlags.Update) {
let tmp: any;
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>);
}
});

const cmptInstance = renderComponent(Cmpt);
const qList = (cmptInstance.query as QueryList<any>);
expect(qList.length).toBe(1);
expect(isElementRef(qList.first)).toBeTruthy();
expect(qList.first.nativeElement).toEqual(elToQuery);
});

/**
* BREAKING CHANGE: this tests asserts different behaviour as compared to Renderer2 when it
* comes to descendants: false option and <ng-container>.
Expand Down