refactor(core): throw more descriptive error message in case of invalid host element (#35916)
This commit replaces an assert with more descriptive error message that is thrown in case `<ng-template>` or `<ng-container>` is used as host element for a Component. Resolves #35240. PR Close #35916
This commit is contained in:
parent
83fe963a4b
commit
0879d2e85d
|
@ -62,7 +62,7 @@
|
||||||
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
||||||
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
||||||
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
||||||
"bundle": 1209688
|
"bundle": 1210239
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
||||||
createElementRef(viewEngine_ElementRef, tElementNode, rootLView), rootLView, tElementNode);
|
createElementRef(viewEngine_ElementRef, tElementNode, rootLView), rootLView, tElementNode);
|
||||||
|
|
||||||
// The host element of the internal root view is attached to the component's host view node.
|
// The host element of the internal root view is attached to the component's host view node.
|
||||||
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, TNodeType.View);
|
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, [TNodeType.View]);
|
||||||
rootTView.node!.child = tElementNode;
|
rootTView.node!.child = tElementNode;
|
||||||
|
|
||||||
return componentRef;
|
return componentRef;
|
||||||
|
|
|
@ -267,7 +267,7 @@ export function diPublicInInjector(
|
||||||
export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): string|null {
|
export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): string|null {
|
||||||
ngDevMode &&
|
ngDevMode &&
|
||||||
assertNodeOfPossibleTypes(
|
assertNodeOfPossibleTypes(
|
||||||
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
|
tNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
|
||||||
ngDevMode && assertDefined(tNode, 'expecting tNode');
|
ngDevMode && assertDefined(tNode, 'expecting tNode');
|
||||||
if (attrNameToInject === 'class') {
|
if (attrNameToInject === 'class') {
|
||||||
return tNode.classes;
|
return tNode.classes;
|
||||||
|
|
|
@ -9,8 +9,7 @@ import {InjectFlags, InjectionToken, resolveForwardRef} from '../../di';
|
||||||
import {ɵɵinject} from '../../di/injector_compatibility';
|
import {ɵɵinject} from '../../di/injector_compatibility';
|
||||||
import {Type} from '../../interface/type';
|
import {Type} from '../../interface/type';
|
||||||
import {getOrCreateInjectable, injectAttributeImpl} from '../di';
|
import {getOrCreateInjectable, injectAttributeImpl} from '../di';
|
||||||
import {TDirectiveHostNode, TNodeType} from '../interfaces/node';
|
import {TDirectiveHostNode} from '../interfaces/node';
|
||||||
import {assertNodeOfPossibleTypes} from '../node_assert';
|
|
||||||
import {getLView, getPreviousOrParentTNode} from '../state';
|
import {getLView, getPreviousOrParentTNode} from '../state';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -128,7 +128,7 @@ function listenerInternal(
|
||||||
|
|
||||||
ngDevMode &&
|
ngDevMode &&
|
||||||
assertNodeOfPossibleTypes(
|
assertNodeOfPossibleTypes(
|
||||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
|
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
|
||||||
|
|
||||||
let processOutputs = true;
|
let processOutputs = true;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGrea
|
||||||
import {createNamedArrayType} from '../../util/named_array_type';
|
import {createNamedArrayType} from '../../util/named_array_type';
|
||||||
import {initNgDevMode} from '../../util/ng_dev_mode';
|
import {initNgDevMode} from '../../util/ng_dev_mode';
|
||||||
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
|
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
|
||||||
|
import {stringify} from '../../util/stringify';
|
||||||
import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert';
|
import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert';
|
||||||
import {attachPatchData} from '../context_discovery';
|
import {attachPatchData} from '../context_discovery';
|
||||||
import {getFactoryDef} from '../definition';
|
import {getFactoryDef} from '../definition';
|
||||||
|
@ -272,7 +273,7 @@ export function assignTViewNodeToLView(
|
||||||
let tNode = tView.node;
|
let tNode = tView.node;
|
||||||
if (tNode == null) {
|
if (tNode == null) {
|
||||||
ngDevMode && tParentNode &&
|
ngDevMode && tParentNode &&
|
||||||
assertNodeOfPossibleTypes(tParentNode, TNodeType.Element, TNodeType.Container);
|
assertNodeOfPossibleTypes(tParentNode, [TNodeType.Element, TNodeType.Container]);
|
||||||
tView.node = tNode = createTNode(
|
tView.node = tNode = createTNode(
|
||||||
tView,
|
tView,
|
||||||
tParentNode as TElementNode | TContainerNode | null, //
|
tParentNode as TElementNode | TContainerNode | null, //
|
||||||
|
@ -1278,7 +1279,7 @@ function instantiateAllDirectives(
|
||||||
const isComponent = isComponentDef(def);
|
const isComponent = isComponentDef(def);
|
||||||
|
|
||||||
if (isComponent) {
|
if (isComponent) {
|
||||||
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element);
|
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element]);
|
||||||
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
|
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1366,7 +1367,7 @@ function findDirectiveDefMatches(
|
||||||
ngDevMode && assertFirstCreatePass(tView);
|
ngDevMode && assertFirstCreatePass(tView);
|
||||||
ngDevMode &&
|
ngDevMode &&
|
||||||
assertNodeOfPossibleTypes(
|
assertNodeOfPossibleTypes(
|
||||||
tNode, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container);
|
tNode, [TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container]);
|
||||||
const registry = tView.directiveRegistry;
|
const registry = tView.directiveRegistry;
|
||||||
let matches: any[]|null = null;
|
let matches: any[]|null = null;
|
||||||
if (registry) {
|
if (registry) {
|
||||||
|
@ -1377,6 +1378,12 @@ function findDirectiveDefMatches(
|
||||||
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);
|
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);
|
||||||
|
|
||||||
if (isComponentDef(def)) {
|
if (isComponentDef(def)) {
|
||||||
|
ngDevMode &&
|
||||||
|
assertNodeOfPossibleTypes(
|
||||||
|
tNode, [TNodeType.Element],
|
||||||
|
`"${tNode.tagName}" tags cannot be used as component hosts. ` +
|
||||||
|
`Please use a different tag to activate the ${
|
||||||
|
stringify(def.type)} component.`);
|
||||||
if (tNode.flags & TNodeFlags.isComponentHost) throwMultipleComponentError(tNode);
|
if (tNode.flags & TNodeFlags.isComponentHost) throwMultipleComponentError(tNode);
|
||||||
markAsComponentHost(tView, tNode);
|
markAsComponentHost(tView, tNode);
|
||||||
// The component is always stored first with directives after.
|
// The component is always stored first with directives after.
|
||||||
|
|
|
@ -26,11 +26,13 @@ export function assertNodeType(tNode: TNode, type: TNodeType): asserts tNode is
|
||||||
assertEqual(tNode.type, type, `should be a ${typeName(type)}`);
|
assertEqual(tNode.type, type, `should be a ${typeName(type)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertNodeOfPossibleTypes(tNode: TNode|null, ...types: TNodeType[]): void {
|
export function assertNodeOfPossibleTypes(
|
||||||
|
tNode: TNode|null, types: TNodeType[], message?: string): void {
|
||||||
assertDefined(tNode, 'should be called with a TNode');
|
assertDefined(tNode, 'should be called with a TNode');
|
||||||
const found = types.some(type => tNode.type === type);
|
const found = types.some(type => tNode.type === type);
|
||||||
assertEqual(
|
assertEqual(
|
||||||
found, true,
|
found, true,
|
||||||
|
message ??
|
||||||
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
|
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -552,7 +552,7 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
|
||||||
} else {
|
} else {
|
||||||
// We are inserting a root element of the component view into the component host element and
|
// We are inserting a root element of the component view into the component host element and
|
||||||
// it should always be eager.
|
// it should always be eager.
|
||||||
ngDevMode && assertNodeOfPossibleTypes(hostTNode, TNodeType.Element);
|
ngDevMode && assertNodeOfPossibleTypes(hostTNode, [TNodeType.Element]);
|
||||||
return currentView[HOST];
|
return currentView[HOST];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -698,10 +698,10 @@ export function appendChild(
|
||||||
*/
|
*/
|
||||||
function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
|
function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
|
||||||
if (tNode !== null) {
|
if (tNode !== null) {
|
||||||
ngDevMode &&
|
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||||
assertNodeOfPossibleTypes(
|
TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.IcuContainer,
|
||||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer,
|
TNodeType.Projection
|
||||||
TNodeType.IcuContainer, TNodeType.Projection);
|
]);
|
||||||
|
|
||||||
const tNodeType = tNode.type;
|
const tNodeType = tNode.type;
|
||||||
if (tNodeType === TNodeType.Element) {
|
if (tNodeType === TNodeType.Element) {
|
||||||
|
@ -778,10 +778,10 @@ function applyNodes(
|
||||||
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
|
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
|
||||||
while (tNode != null) {
|
while (tNode != null) {
|
||||||
ngDevMode && assertTNodeForLView(tNode, lView);
|
ngDevMode && assertTNodeForLView(tNode, lView);
|
||||||
ngDevMode &&
|
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||||
assertNodeOfPossibleTypes(
|
TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Projection,
|
||||||
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer,
|
TNodeType.IcuContainer
|
||||||
TNodeType.Projection, TNodeType.Projection, TNodeType.IcuContainer);
|
]);
|
||||||
const rawSlotValue = lView[tNode.index];
|
const rawSlotValue = lView[tNode.index];
|
||||||
const tNodeType = tNode.type;
|
const tNodeType = tNode.type;
|
||||||
if (isProjection) {
|
if (isProjection) {
|
||||||
|
@ -798,7 +798,7 @@ function applyNodes(
|
||||||
applyProjectionRecursive(
|
applyProjectionRecursive(
|
||||||
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
|
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
|
||||||
} else {
|
} else {
|
||||||
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container);
|
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element, TNodeType.Container]);
|
||||||
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
|
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,7 @@ function createSpecialToken(lView: LView, tNode: TNode, read: any): any {
|
||||||
} else if (read === ViewContainerRef) {
|
} else if (read === ViewContainerRef) {
|
||||||
ngDevMode &&
|
ngDevMode &&
|
||||||
assertNodeOfPossibleTypes(
|
assertNodeOfPossibleTypes(
|
||||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
|
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
|
||||||
return createContainerRef(
|
return createContainerRef(
|
||||||
ViewContainerRef, ViewEngine_ElementRef,
|
ViewContainerRef, ViewEngine_ElementRef,
|
||||||
tNode as TElementNode | TContainerNode | TElementContainerNode, lView);
|
tNode as TElementNode | TContainerNode | TElementContainerNode, lView);
|
||||||
|
|
|
@ -340,7 +340,7 @@ export function createContainerRef(
|
||||||
|
|
||||||
ngDevMode &&
|
ngDevMode &&
|
||||||
assertNodeOfPossibleTypes(
|
assertNodeOfPossibleTypes(
|
||||||
hostTNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
|
hostTNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
|
||||||
|
|
||||||
let lContainer: LContainer;
|
let lContainer: LContainer;
|
||||||
const slotValue = hostView[hostTNode.index];
|
const slotValue = hostView[hostTNode.index];
|
||||||
|
|
|
@ -324,10 +324,10 @@ function collectNativeNodes(
|
||||||
tView: TView, lView: LView, tNode: TNode|null, result: any[],
|
tView: TView, lView: LView, tNode: TNode|null, result: any[],
|
||||||
isProjection: boolean = false): any[] {
|
isProjection: boolean = false): any[] {
|
||||||
while (tNode !== null) {
|
while (tNode !== null) {
|
||||||
ngDevMode &&
|
ngDevMode && assertNodeOfPossibleTypes(tNode, [
|
||||||
assertNodeOfPossibleTypes(
|
TNodeType.Element, TNodeType.Container, TNodeType.Projection, TNodeType.ElementContainer,
|
||||||
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection,
|
TNodeType.IcuContainer
|
||||||
TNodeType.ElementContainer, TNodeType.IcuContainer);
|
]);
|
||||||
|
|
||||||
const lNode = lView[tNode.index];
|
const lNode = lView[tNode.index];
|
||||||
if (lNode !== null) {
|
if (lNode !== null) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, Eleme
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
|
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
import {onlyInIvy} from '@angular/private/testing';
|
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
||||||
|
|
||||||
import {domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
import {domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||||
|
|
||||||
|
@ -259,6 +259,65 @@ describe('component', () => {
|
||||||
expect(wrapperEls.length).toBe(2); // other elements are preserved
|
expect(wrapperEls.length).toBe(2); // other elements are preserved
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('invalid host element', () => {
|
||||||
|
it('should throw when <ng-container> is used as a host element for a Component', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ng-container',
|
||||||
|
template: '...',
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'root',
|
||||||
|
template: '<ng-container></ng-container>',
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App, Comp]});
|
||||||
|
if (ivyEnabled) {
|
||||||
|
expect(() => TestBed.createComponent(App))
|
||||||
|
.toThrowError(
|
||||||
|
/"ng-container" tags cannot be used as component hosts. Please use a different tag to activate the Comp component/);
|
||||||
|
} else {
|
||||||
|
// In VE there is no special check for the case when `<ng-container>` is used as a host
|
||||||
|
// element for a Component. VE tries to attach Component's content to a Comment node that
|
||||||
|
// represents the `<ng-container>` location and this call fails with a
|
||||||
|
// browser/environment-specific error message, so we just verify that this scenario is
|
||||||
|
// triggering an error in VE.
|
||||||
|
expect(() => TestBed.createComponent(App)).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when <ng-template> is used as a host element for a Component', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'ng-template',
|
||||||
|
template: '...',
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'root',
|
||||||
|
template: '<ng-template></ng-template>',
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App, Comp]});
|
||||||
|
if (ivyEnabled) {
|
||||||
|
expect(() => TestBed.createComponent(App))
|
||||||
|
.toThrowError(
|
||||||
|
/"ng-template" tags cannot be used as component hosts. Please use a different tag to activate the Comp component/);
|
||||||
|
} else {
|
||||||
|
expect(() => TestBed.createComponent(App))
|
||||||
|
.toThrowError(
|
||||||
|
/Components on an embedded template: Comp \("\[ERROR ->\]<ng-template><\/ng-template>"\)/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should use a new ngcontent attribute for child elements created w/ Renderer2', () => {
|
it('should use a new ngcontent attribute for child elements created w/ Renderer2', () => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
|
Loading…
Reference in New Issue