mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): better error message when unknown property is present (#46147)
Prior to this commit, the error message that was produced for the unknown property situation, din't contain extra info on how the problem can be fixed. This commit adds more info to the error message and makes it similar to the one we use during the AOT compilation. PR Close #46147
This commit is contained in:
committed by
Andrew Scott
parent
542d4c7805
commit
0bc77f4cab
@@ -255,7 +255,7 @@ function validateElementIsKnown(
|
||||
let message = `'${tagName}' is not a known element:\n`;
|
||||
message += `1. If '${tagName}' is an Angular component, then verify that it is ${
|
||||
hostIsStandalone ? 'included in the \'@Component.imports\' of this component' :
|
||||
'a part of this module'}.\n`;
|
||||
'a part of an @NgModule where this component is declared'}.\n`;
|
||||
if (tagName && tagName.indexOf('-') > -1) {
|
||||
message +=
|
||||
`2. If '${tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${
|
||||
|
||||
@@ -1053,7 +1053,7 @@ export function elementPropertyInternal<T>(
|
||||
validateAgainstEventProperties(propName);
|
||||
if (!validateProperty(element, tNode.value, propName, tView.schemas)) {
|
||||
// Return here since we only log warnings for unknown properties.
|
||||
handleUnknownPropertyError(propName, tNode);
|
||||
handleUnknownPropertyError(propName, tNode, isHostComponentStandalone(lView));
|
||||
return;
|
||||
}
|
||||
ngDevMode.rendererSetProperty++;
|
||||
@@ -1072,7 +1072,7 @@ export function elementPropertyInternal<T>(
|
||||
// If the node is a container and the property didn't
|
||||
// match any of the inputs or schemas we should throw.
|
||||
if (ngDevMode && !matchingSchemas(tView.schemas, tNode.value)) {
|
||||
handleUnknownPropertyError(propName, tNode);
|
||||
handleUnknownPropertyError(propName, tNode, isHostComponentStandalone(lView));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1184,12 +1184,22 @@ export function matchingSchemas(schemas: SchemaMetadata[]|null, tagName: string|
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The set of known control flow directives.
|
||||
* We use this set to produce a more precises error message with a note
|
||||
* that the `CommonModule` should also be included.
|
||||
*/
|
||||
export const KNOWN_CONTROL_FLOW_DIRECTIVES =
|
||||
new Set(['ngIf', 'ngFor', 'ngSwitch', 'ngSwitchCase', 'ngSwitchDefault']);
|
||||
|
||||
/**
|
||||
* Logs or throws an error that a property is not supported on an element.
|
||||
* @param propName Name of the invalid property.
|
||||
* @param tagName Name of the node on which we encountered the property.
|
||||
* @param hostIsStandalone Boolean indicating whether the host is a standalone component.
|
||||
*/
|
||||
function handleUnknownPropertyError(propName: string, tNode: TNode): void {
|
||||
function handleUnknownPropertyError(
|
||||
propName: string, tNode: TNode, hostIsStandalone: boolean): void {
|
||||
let tagName = tNode.value;
|
||||
|
||||
// Special-case a situation when a structural directive is applied to
|
||||
@@ -1201,7 +1211,34 @@ function handleUnknownPropertyError(propName: string, tNode: TNode): void {
|
||||
if (!tagName && tNode.type === TNodeType.Container) {
|
||||
tagName = 'ng-template';
|
||||
}
|
||||
const message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'.`;
|
||||
let message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'.`;
|
||||
|
||||
const schemas = `'${hostIsStandalone ? '@Component' : '@NgModule'}.schemas'`;
|
||||
const importLocation = hostIsStandalone ?
|
||||
'included in the \'@Component.imports\' of this component' :
|
||||
'a part of an @NgModule where this component is declared';
|
||||
if (KNOWN_CONTROL_FLOW_DIRECTIVES.has(propName)) {
|
||||
// Most likely this is a control flow directive (such as `*ngIf`) used in
|
||||
// a template, but the `CommonModule` is not imported.
|
||||
message += `\nIf the '${propName}' is an Angular control flow directive, ` +
|
||||
`please make sure that the 'CommonModule' is ${importLocation}.`;
|
||||
} else {
|
||||
// May be an Angular component, which is not imported/declared?
|
||||
message += `\n1. If '${tagName}' is an Angular component and it has the ` +
|
||||
`'${propName}' input, then verify that it is ${importLocation}.`;
|
||||
// May be a Web Component?
|
||||
if (tagName && tagName.indexOf('-') > -1) {
|
||||
message += `\n2. If '${tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' ` +
|
||||
`to the ${schemas} of this component to suppress this message.`;
|
||||
message += `\n3. To allow any property add 'NO_ERRORS_SCHEMA' to ` +
|
||||
`the ${schemas} of this component.`;
|
||||
} else {
|
||||
// If it's expected, the error can be suppressed by the `NO_ERRORS_SCHEMA` schema.
|
||||
message += `\n2. To allow any property add 'NO_ERRORS_SCHEMA' to ` +
|
||||
`the ${schemas} of this component.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldThrowErrorOnUnknownProperty) {
|
||||
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message);
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, createNgModuleRef, CUSTOM_ELEMENTS_SCHEMA, destroyPlatform, Directive, Injectable, InjectionToken, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, Pipe, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelement as element, ɵɵproperty as property} from '@angular/core';
|
||||
import {KNOWN_CONTROL_FLOW_DIRECTIVES} from '@angular/core/src/render3/instructions/shared';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
@@ -590,6 +591,106 @@ describe('NgModule', () => {
|
||||
/NG0303: Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container'/g);
|
||||
});
|
||||
|
||||
it('should log an error on unknown props and include a note on Web Components', () => {
|
||||
@Component({
|
||||
selector: 'may-be-web-component',
|
||||
template: `...`,
|
||||
})
|
||||
class MaybeWebComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-comp',
|
||||
template: `<may-be-web-component [unknownProp]="condition"></may-be-web-component>`,
|
||||
})
|
||||
class MyComp {
|
||||
condition = true;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyComp, MaybeWebComp],
|
||||
})
|
||||
class MyModule {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({imports: [MyModule]});
|
||||
|
||||
const spy = spyOn(console, 'error');
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
const errorMessage = spy.calls.mostRecent().args[0];
|
||||
|
||||
// Split the error message into chunks, so it's easier to debug if needed.
|
||||
const lines = [
|
||||
`NG0303: Can't bind to 'unknownProp' since it isn't a known property of 'may-be-web-component'.`,
|
||||
`1. If 'may-be-web-component' is an Angular component and it has the 'unknownProp' input, then verify that it is a part of an @NgModule where this component is declared.`,
|
||||
`2. If 'may-be-web-component' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`,
|
||||
`3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`
|
||||
];
|
||||
lines.forEach(line => expect(errorMessage).toMatch(line));
|
||||
});
|
||||
|
||||
KNOWN_CONTROL_FLOW_DIRECTIVES.forEach(directive => {
|
||||
it(`should produce a warning when the '${directive}' directive ` +
|
||||
`is used in a template, but not imported in corresponding NgModule`,
|
||||
() => {
|
||||
@Component({
|
||||
template: `<div *${directive}="expr"></div>`,
|
||||
})
|
||||
class App {
|
||||
expr = true;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [App],
|
||||
})
|
||||
class Module {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({imports: [Module]});
|
||||
const spy = spyOn(console, 'error');
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const errorMessage = spy.calls.mostRecent()?.args[0];
|
||||
|
||||
// Split the error message into chunks, so it's easier to debug if needed.
|
||||
const lines = [
|
||||
`NG0303: Can't bind to '${directive}' since it isn't a known property of 'div'.`,
|
||||
`If the '${directive}' is an Angular control flow directive, please make sure ` +
|
||||
`that the 'CommonModule' is a part of an @NgModule where this component is declared.`
|
||||
];
|
||||
lines.forEach(line => expect(errorMessage).toMatch(line));
|
||||
});
|
||||
|
||||
it(`should produce a warning when the '${directive}' directive ` +
|
||||
`is used in a template, but not imported in a standalone component`,
|
||||
() => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `<div *${directive}="expr"></div>`,
|
||||
})
|
||||
class App {
|
||||
expr = true;
|
||||
}
|
||||
|
||||
const spy = spyOn(console, 'error');
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const errorMessage = spy.calls.mostRecent()?.args[0];
|
||||
|
||||
// Split the error message into chunks, so it's easier to debug if needed.
|
||||
const lines = [
|
||||
`NG0303: Can't bind to '${directive}' since it isn't a known property of 'div'.`,
|
||||
`If the '${directive}' is an Angular control flow directive, please make sure ` +
|
||||
`that the 'CommonModule' is included in the '@Component.imports' of this component.`
|
||||
];
|
||||
lines.forEach(line => expect(errorMessage).toMatch(line));
|
||||
});
|
||||
});
|
||||
|
||||
describe('AOT-compiled components', () => {
|
||||
function createComponent(
|
||||
template: (rf: any) => void, vars: number, consts?: (number|string)[][]) {
|
||||
|
||||
Reference in New Issue
Block a user