Skip to content

Commit b1a4b30

Browse files
dario-piotrowiczalxhub
authored andcommitted
fix(core): update unknown tag error for jit standalone components (#45920)
update the error message presented during jit compilation when an unrecognized tag/element is found in a standalone component so that it does not mention the ngModule anymore Note: the aot variant is present in PR #45919 resolves #45818 PR Close #45920
1 parent dfba192 commit b1a4b30

3 files changed

Lines changed: 112 additions & 10 deletions

File tree

packages/core/src/render3/instructions/element.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {setUpAttributes} from '../util/attrs_utils';
2424
import {getConstant} from '../util/view_utils';
2525

2626
import {setDirectiveInputsWhichShadowsStyling} from './property';
27-
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
27+
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, isHostComponentStandalone, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
2828

2929
let shouldThrowErrorOnUnknownElement = false;
3030

@@ -56,7 +56,10 @@ function elementStartFirstCreatePass(
5656

5757
const hasDirectives =
5858
resolveDirectives(tView, lView, tNode, getConstant<string[]>(tViewConsts, localRefsIndex));
59-
ngDevMode && validateElementIsKnown(native, tNode.value, tView.schemas, hasDirectives);
59+
if (ngDevMode) {
60+
const hostIsStandalone = isHostComponentStandalone(lView);
61+
validateElementIsKnown(native, tNode.value, tView.schemas, hasDirectives, hostIsStandalone);
62+
}
6063

6164
if (tNode.attrs !== null) {
6265
computeStaticStyling(tNode, tNode.attrs, false);
@@ -223,10 +226,11 @@ export function ɵɵelement(
223226
* @param tagName Name of the tag to check
224227
* @param schemas Array of schemas
225228
* @param hasDirectives Boolean indicating that the element matches any directive
229+
* @param hostIsStandalone Boolean indicating whether the host is a standalone component
226230
*/
227231
function validateElementIsKnown(
228-
element: RElement, tagName: string|null, schemas: SchemaMetadata[]|null,
229-
hasDirectives: boolean): void {
232+
element: RElement, tagName: string|null, schemas: SchemaMetadata[]|null, hasDirectives: boolean,
233+
hostIsStandalone: boolean): void {
230234
// If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT
231235
// mode where this check happens at compile time. In JIT mode, `schemas` is always present and
232236
// defined as an array (as an empty array in case `schemas` field is not defined) and we should
@@ -247,15 +251,18 @@ function validateElementIsKnown(
247251
!customElements.get(tagName));
248252

249253
if (isUnknown && !matchingSchemas(schemas, tagName)) {
254+
const schemas = `'${hostIsStandalone ? '@Component' : '@NgModule'}.schemas'`;
250255
let message = `'${tagName}' is not a known element:\n`;
251-
message += `1. If '${
252-
tagName}' is an Angular component, then verify that it is part of this module.\n`;
256+
message += `1. If '${tagName}' is an Angular component, then verify that it is ${
257+
hostIsStandalone ? 'included in the \'@Component.imports\' of this component' :
258+
'a part of this module'}.\n`;
253259
if (tagName && tagName.indexOf('-') > -1) {
254-
message += `2. If '${
255-
tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`;
260+
message +=
261+
`2. If '${tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${
262+
schemas} of this component to suppress this message.`;
256263
} else {
257264
message +=
258-
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
265+
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`;
259266
}
260267
if (shouldThrowErrorOnUnknownElement) {
261268
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message);

packages/core/src/render3/instructions/shared.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ import {Injector} from '../../di';
99
import {ErrorHandler} from '../../error_handler';
1010
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../../errors';
1111
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
12+
import {Type} from '../../interface/type';
1213
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema';
1314
import {ViewEncapsulation} from '../../metadata/view';
1415
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../../sanitization/sanitization';
1516
import {Sanitizer} from '../../sanitization/sanitizer';
16-
import {assertDefined, assertDomNode, assertEqual, assertGreaterThanOrEqual, assertIndexInRange, assertNotEqual, assertNotSame, assertSame, assertString} from '../../util/assert';
17+
import {assertDefined, assertDomNode, assertEqual, assertGreaterThanOrEqual, assertIndexInRange, assertNotEqual, assertNotSame, assertSame, assertString, throwError} from '../../util/assert';
1718
import {escapeCommentText} from '../../util/dom';
1819
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
1920
import {stringify} from '../../util/stringify';
2021
import {assertFirstCreatePass, assertFirstUpdatePass, assertLContainer, assertLView, assertTNodeForLView, assertTNodeForTView} from '../assert';
2122
import {attachPatchData, readPatchedLView} from '../context_discovery';
23+
import {getComponentDef} from '../definition';
2224
import {getFactoryDef} from '../definition_factory';
2325
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
2426
import {throwMultipleComponentError} from '../errors';
@@ -266,6 +268,23 @@ export function createTNodeAtIndex(
266268
return tNode;
267269
}
268270

271+
/**
272+
* Checks if the current component is declared inside of a standalone component template.
273+
*
274+
* @param lView An `LView` that represents a current component that is being rendered.
275+
*/
276+
export function isHostComponentStandalone(lView: LView): boolean {
277+
!ngDevMode && throwError('Must never be called in production mode');
278+
279+
const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView<Type<unknown>>;
280+
const context = declarationLView[CONTEXT];
281+
282+
// Unable to obtain a context, fall back to the non-standalone scenario.
283+
if (!context) return false;
284+
285+
const componentDef = getComponentDef(context.constructor);
286+
return !!(componentDef?.standalone);
287+
}
269288

270289
/**
271290
* When elements are created dynamically after a view blueprint is created (e.g. through

packages/core/test/acceptance/standalone_spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,82 @@ describe('standalone components, directives and pipes', () => {
630630
});
631631
});
632632

633+
describe('unknown template elements', () => {
634+
const unknownElErrorRegex = (tag: string) => {
635+
const prefix = `'${tag}' is not a known element:`;
636+
const message1 = `1. If '${
637+
tag}' is an Angular component, then verify that it is included in the '@Component.imports' of this component.`;
638+
const message2 = `2. If '${
639+
tag}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.`;
640+
return new RegExp(`${prefix}\s*\n\s*${message1}\s*\n\s*${message2}`);
641+
};
642+
643+
it('should warn the user when an unknown element is present', () => {
644+
const spy = spyOn(console, 'error');
645+
@Component({
646+
standalone: true,
647+
template: '<unknown-tag></unknown-tag>',
648+
})
649+
class AppCmp {
650+
}
651+
652+
TestBed.createComponent(AppCmp);
653+
654+
const errorRegex = unknownElErrorRegex('unknown-tag');
655+
expect(spy).toHaveBeenCalledOnceWith(jasmine.stringMatching(errorRegex));
656+
});
657+
658+
it('should warn the user when multiple unknown elements are present', () => {
659+
const spy = spyOn(console, 'error');
660+
@Component({
661+
standalone: true,
662+
template: '<unknown-tag-A></unknown-tag-A><unknown-tag-B></unknown-tag-B>',
663+
})
664+
class AppCmp {
665+
}
666+
667+
TestBed.createComponent(AppCmp);
668+
669+
const errorRegexA = unknownElErrorRegex('unknown-tag-A');
670+
const errorRegexB = unknownElErrorRegex('unknown-tag-B');
671+
672+
expect(spy).toHaveBeenCalledWith(jasmine.stringMatching(errorRegexA));
673+
expect(spy).toHaveBeenCalledWith(jasmine.stringMatching(errorRegexB));
674+
});
675+
676+
it('should not warn the user when an unknown element is present inside an ng-template', () => {
677+
const spy = spyOn(console, 'error');
678+
@Component({
679+
standalone: true,
680+
template: '<ng-template><unknown-tag></unknown-tag><ng-template>',
681+
})
682+
class AppCmp {
683+
}
684+
685+
TestBed.createComponent(AppCmp);
686+
687+
expect(spy).not.toHaveBeenCalled();
688+
});
689+
690+
it('should warn the user when an unknown element is present in an instantiated embedded view',
691+
() => {
692+
const spy = spyOn(console, 'error');
693+
@Component({
694+
standalone: true,
695+
template: '<ng-template [ngIf]="true"><unknown-tag></unknown-tag><ng-template>',
696+
imports: [CommonModule],
697+
})
698+
class AppCmp {
699+
}
700+
701+
const fixture = TestBed.createComponent(AppCmp);
702+
fixture.detectChanges();
703+
704+
const errorRegex = unknownElErrorRegex('unknown-tag');
705+
expect(spy).toHaveBeenCalledOnceWith(jasmine.stringMatching(errorRegex));
706+
});
707+
});
708+
633709
/*
634710
The following test verify that we don't impose limits when it comes to extending components of
635711
various type (standalone vs. non-standalone).

0 commit comments

Comments
 (0)