diff --git a/packages/core/src/render3/instructions/all.ts b/packages/core/src/render3/instructions/all.ts index 723aedc6d07..b4d04730080 100644 --- a/packages/core/src/render3/instructions/all.ts +++ b/packages/core/src/render3/instructions/all.ts @@ -50,4 +50,4 @@ export * from './style_map_interpolation'; export * from './style_prop_interpolation'; export * from './host_property'; export * from './i18n'; -export {ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode} from './shared'; +export {ɵgetUnknownElementStrictMode, ɵsetUnknownElementStrictMode, ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode} from './element_validation'; diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 3eb0625c4f6..003f2204656 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../../errors'; -import {SchemaMetadata} from '../../metadata/schema'; import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert'; import {assertFirstCreatePass, assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; @@ -23,26 +21,10 @@ import {computeStaticStyling} from '../styling/static_styling'; import {setUpAttributes} from '../util/attrs_utils'; import {getConstant} from '../util/view_utils'; +import {validateElementIsKnown} from './element_validation'; import {setDirectiveInputsWhichShadowsStyling} from './property'; -import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, getTemplateLocationDetails, isHostComponentStandalone, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared'; +import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared'; -let shouldThrowErrorOnUnknownElement = false; - -/** - * Sets a strict mode for JIT-compiled components to throw an error on unknown elements, - * instead of just logging the error. - * (for AOT-compiled ones this check happens at build time). - */ -export function ɵsetUnknownElementStrictMode(shouldThrow: boolean) { - shouldThrowErrorOnUnknownElement = shouldThrow; -} - -/** - * Gets the current value of the strict mode. - */ -export function ɵgetUnknownElementStrictMode() { - return shouldThrowErrorOnUnknownElement; -} function elementStartFirstCreatePass( index: number, tView: TView, lView: LView, native: RElement, name: string, @@ -208,69 +190,3 @@ export function ɵɵelement( ɵɵelementEnd(); return ɵɵelement; } - -/** - * Validates that the element is known at runtime and produces - * an error if it's not the case. - * This check is relevant for JIT-compiled components (for AOT-compiled - * ones this check happens at build time). - * - * The element is considered known if either: - * - it's a known HTML element - * - it's a known custom element - * - the element matches any directive - * - the element is allowed by one of the schemas - * - * @param element Element to validate - * @param lView An `LView` that represents a current component that is being rendered. - * @param tagName Name of the tag to check - * @param schemas Array of schemas - * @param hasDirectives Boolean indicating that the element matches any directive - */ -function validateElementIsKnown( - element: RElement, lView: LView, tagName: string|null, schemas: SchemaMetadata[]|null, - hasDirectives: boolean): void { - // If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT - // mode where this check happens at compile time. In JIT mode, `schemas` is always present and - // defined as an array (as an empty array in case `schemas` field is not defined) and we should - // execute the check below. - if (schemas === null) return; - - // If the element matches any directive, it's considered as valid. - if (!hasDirectives && tagName !== null) { - // The element is unknown if it's an instance of HTMLUnknownElement, or it isn't registered - // as a custom element. Note that unknown elements with a dash in their name won't be instances - // of HTMLUnknownElement in browsers that support web components. - const isUnknown = - // Note that we can't check for `typeof HTMLUnknownElement === 'function'`, - // because while most browsers return 'function', IE returns 'object'. - (typeof HTMLUnknownElement !== 'undefined' && HTMLUnknownElement && - element instanceof HTMLUnknownElement) || - (typeof customElements !== 'undefined' && tagName.indexOf('-') > -1 && - !customElements.get(tagName)); - - if (isUnknown && !matchingSchemas(schemas, tagName)) { - const isHostStandalone = isHostComponentStandalone(lView); - const templateLocation = getTemplateLocationDetails(lView); - const schemas = `'${isHostStandalone ? '@Component' : '@NgModule'}.schemas'`; - - let message = `'${tagName}' is not a known element${templateLocation}:\n`; - message += `1. If '${tagName}' is an Angular component, then verify that it is ${ - isHostStandalone ? 'included in the \'@Component.imports\' of this component' : - '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 ${ - schemas} of this component to suppress this message.`; - } else { - message += - `2. To allow any element add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`; - } - if (shouldThrowErrorOnUnknownElement) { - throw new RuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message); - } else { - console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message)); - } - } - } -} diff --git a/packages/core/src/render3/instructions/element_validation.ts b/packages/core/src/render3/instructions/element_validation.ts new file mode 100644 index 00000000000..6cfc906373b --- /dev/null +++ b/packages/core/src/render3/instructions/element_validation.ts @@ -0,0 +1,298 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../../errors'; +import {Type} from '../../interface/type'; +import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema'; +import {throwError} from '../../util/assert'; +import {getComponentDef} from '../definition'; +import {ComponentDef} from '../interfaces/definition'; +import {TNodeType} from '../interfaces/node'; +import {RComment, RElement} from '../interfaces/renderer_dom'; +import {CONTEXT, DECLARATION_COMPONENT_VIEW, LView} from '../interfaces/view'; +import {isAnimationProp} from '../util/attrs_utils'; + +let shouldThrowErrorOnUnknownElement = false; + +/** + * Sets a strict mode for JIT-compiled components to throw an error on unknown elements, + * instead of just logging the error. + * (for AOT-compiled ones this check happens at build time). + */ +export function ɵsetUnknownElementStrictMode(shouldThrow: boolean) { + shouldThrowErrorOnUnknownElement = shouldThrow; +} + +/** + * Gets the current value of the strict mode. + */ +export function ɵgetUnknownElementStrictMode() { + return shouldThrowErrorOnUnknownElement; +} + +let shouldThrowErrorOnUnknownProperty = false; + +/** + * Sets a strict mode for JIT-compiled components to throw an error on unknown properties, + * instead of just logging the error. + * (for AOT-compiled ones this check happens at build time). + */ +export function ɵsetUnknownPropertyStrictMode(shouldThrow: boolean) { + shouldThrowErrorOnUnknownProperty = shouldThrow; +} + +/** + * Gets the current value of the strict mode. + */ +export function ɵgetUnknownPropertyStrictMode() { + return shouldThrowErrorOnUnknownProperty; +} + +/** + * Validates that the element is known at runtime and produces + * an error if it's not the case. + * This check is relevant for JIT-compiled components (for AOT-compiled + * ones this check happens at build time). + * + * The element is considered known if either: + * - it's a known HTML element + * - it's a known custom element + * - the element matches any directive + * - the element is allowed by one of the schemas + * + * @param element Element to validate + * @param lView An `LView` that represents a current component that is being rendered + * @param tagName Name of the tag to check + * @param schemas Array of schemas + * @param hasDirectives Boolean indicating that the element matches any directive + */ +export function validateElementIsKnown( + element: RElement, lView: LView, tagName: string|null, schemas: SchemaMetadata[]|null, + hasDirectives: boolean): void { + // If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT + // mode where this check happens at compile time. In JIT mode, `schemas` is always present and + // defined as an array (as an empty array in case `schemas` field is not defined) and we should + // execute the check below. + if (schemas === null) return; + + // If the element matches any directive, it's considered as valid. + if (!hasDirectives && tagName !== null) { + // The element is unknown if it's an instance of HTMLUnknownElement, or it isn't registered + // as a custom element. Note that unknown elements with a dash in their name won't be instances + // of HTMLUnknownElement in browsers that support web components. + const isUnknown = + // Note that we can't check for `typeof HTMLUnknownElement === 'function'`, + // because while most browsers return 'function', IE returns 'object'. + (typeof HTMLUnknownElement !== 'undefined' && HTMLUnknownElement && + element instanceof HTMLUnknownElement) || + (typeof customElements !== 'undefined' && tagName.indexOf('-') > -1 && + !customElements.get(tagName)); + + if (isUnknown && !matchingSchemas(schemas, tagName)) { + const isHostStandalone = isHostComponentStandalone(lView); + const templateLocation = getTemplateLocationDetails(lView); + const schemas = `'${isHostStandalone ? '@Component' : '@NgModule'}.schemas'`; + + let message = `'${tagName}' is not a known element${templateLocation}:\n`; + message += `1. If '${tagName}' is an Angular component, then verify that it is ${ + isHostStandalone ? 'included in the \'@Component.imports\' of this component' : + '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 ${ + schemas} of this component to suppress this message.`; + } else { + message += + `2. To allow any element add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`; + } + if (shouldThrowErrorOnUnknownElement) { + throw new RuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message); + } else { + console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message)); + } + } + } +} + +/** + * Validates that the property of the element is known at runtime and returns + * false if it's not the case. + * This check is relevant for JIT-compiled components (for AOT-compiled + * ones this check happens at build time). + * + * The property is considered known if either: + * - it's a known property of the element + * - the element is allowed by one of the schemas + * - the property is used for animations + * + * @param element Element to validate + * @param propName Name of the property to check + * @param tagName Name of the tag hosting the property + * @param schemas Array of schemas + */ +export function isPropertyValid( + element: RElement|RComment, propName: string, tagName: string|null, + schemas: SchemaMetadata[]|null): boolean { + // If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT + // mode where this check happens at compile time. In JIT mode, `schemas` is always present and + // defined as an array (as an empty array in case `schemas` field is not defined) and we should + // execute the check below. + if (schemas === null) return true; + + // The property is considered valid if the element matches the schema, it exists on the element, + // or it is synthetic, and we are in a browser context (web worker nodes should be skipped). + if (matchingSchemas(schemas, tagName) || propName in element || isAnimationProp(propName)) { + return true; + } + + // Note: `typeof Node` returns 'function' in most browsers, but on IE it is 'object' so we + // need to account for both here, while being careful with `typeof null` also returning 'object'. + return typeof Node === 'undefined' || Node === null || !(element instanceof Node); +} + +/** + * 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 tag hosting the property + * @param nodeType Type of the node hosting the property + * @param lView An `LView` that represents a current component + */ +export function handleUnknownPropertyError( + propName: string, tagName: string|null, nodeType: TNodeType, lView: LView): void { + // Special-case a situation when a structural directive is applied to + // an `` element, for example: ``. + // In this case the compiler generates the `ɵɵtemplate` instruction with + // the `null` as the tagName. The directive matching logic at runtime relies + // on this effect (see `isInlineTemplate`), thus using the 'ng-template' as + // a default value of the `tNode.value` is not feasible at this moment. + if (!tagName && nodeType === TNodeType.Container) { + tagName = 'ng-template'; + } + + const isHostStandalone = isHostComponentStandalone(lView); + const templateLocation = getTemplateLocationDetails(lView); + + let message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'${ + templateLocation}.`; + + const schemas = `'${isHostStandalone ? '@Component' : '@NgModule'}.schemas'`; + const importLocation = isHostStandalone ? + '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 { + console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message)); + } +} + +/** + * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`) + * and must **not** be used in production bundles. The function makes megamorphic reads, which might + * be too slow for production mode and also it relies on the constructor function being available. + * + * Gets a reference to the host component def (where a current component is declared). + * + * @param lView An `LView` that represents a current component that is being rendered. + */ +function getDeclarationComponentDef(lView: LView): ComponentDef|null { + !ngDevMode && throwError('Must never be called in production mode'); + + const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView>; + const context = declarationLView[CONTEXT]; + + // Unable to obtain a context. + if (!context) return null; + + return context.constructor ? getComponentDef(context.constructor) : null; +} + +/** + * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`) + * and must **not** be used in production bundles. The function makes megamorphic reads, which might + * be too slow for production mode. + * + * Checks if the current component is declared inside of a standalone component template. + * + * @param lView An `LView` that represents a current component that is being rendered. + */ +function isHostComponentStandalone(lView: LView): boolean { + !ngDevMode && throwError('Must never be called in production mode'); + + const componentDef = getDeclarationComponentDef(lView); + // Treat host component as non-standalone if we can't obtain the def. + return !!componentDef?.standalone; +} + +/** + * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`) + * and must **not** be used in production bundles. The function makes megamorphic reads, which might + * be too slow for production mode. + * + * Constructs a string describing the location of the host component template. The function is used + * in dev mode to produce error messages. + * + * @param lView An `LView` that represents a current component that is being rendered. + */ +function getTemplateLocationDetails(lView: LView): string { + !ngDevMode && throwError('Must never be called in production mode'); + + const hostComponentDef = getDeclarationComponentDef(lView); + const componentClassName = hostComponentDef?.type?.name; + return componentClassName ? ` (used in the '${componentClassName}' component template)` : ''; +} + +/** + * 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']); + +/** + * Returns true if the tag name is allowed by specified schemas. + * @param schemas Array of schemas + * @param tagName Name of the tag + */ +export function matchingSchemas(schemas: SchemaMetadata[]|null, tagName: string|null): boolean { + if (schemas !== null) { + for (let i = 0; i < schemas.length; i++) { + const schema = schemas[i]; + if (schema === NO_ERRORS_SCHEMA || + schema === CUSTOM_ELEMENTS_SCHEMA && tagName && tagName.indexOf('-') > -1) { + return true; + } + } + } + + return false; +} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index cd8c700138e..41bd3ac3431 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -20,7 +20,6 @@ import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ import {stringify} from '../../util/stringify'; import {assertFirstCreatePass, assertFirstUpdatePass, assertLContainer, assertLView, assertTNodeForLView, assertTNodeForTView} from '../assert'; import {attachPatchData, readPatchedLView} from '../context_discovery'; -import {getComponentDef} from '../definition'; import {getFactoryDef} from '../definition_factory'; import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di'; import {throwMultipleComponentError} from '../errors'; @@ -49,26 +48,9 @@ import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreation import {selectIndexInternal} from './advance'; import {ɵɵdirectiveInject} from './di'; +import {handleUnknownPropertyError, isPropertyValid, matchingSchemas} from './element_validation'; import {attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData, LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor} from './lview_debug'; -let shouldThrowErrorOnUnknownProperty = false; - -/** - * Sets a strict mode for JIT-compiled components to throw an error on unknown properties, - * instead of just logging the error. - * (for AOT-compiled ones this check happens at build time). - */ -export function ɵsetUnknownPropertyStrictMode(shouldThrow: boolean) { - shouldThrowErrorOnUnknownProperty = shouldThrow; -} - -/** - * Gets the current value of the strict mode. - */ -export function ɵgetUnknownPropertyStrictMode() { - return shouldThrowErrorOnUnknownProperty; -} - /** * A permanent marker promise which signifies that the current CD tree is * clean. @@ -268,62 +250,6 @@ export function createTNodeAtIndex( return tNode; } -/** - * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`) - * and must **not** be used in production bundles. The function makes megamorphic reads, which might - * be too slow for production mode and also it relies on the constructor function being available. - * - * Gets a reference to the host component def (where a current component is declared). - * - * @param lView An `LView` that represents a current component that is being rendered. - */ -function getDeclarationComponentDef(lView: LView): ComponentDef|null { - !ngDevMode && throwError('Must never be called in production mode'); - - const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView>; - const context = declarationLView[CONTEXT]; - - // Unable to obtain a context. - if (!context) return null; - - return context.constructor ? getComponentDef(context.constructor) : null; -} - -/** - * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`) - * and must **not** be used in production bundles. The function makes megamorphic reads, which might - * be too slow for production mode. - * - * Checks if the current component is declared inside of a standalone component template. - * - * @param lView An `LView` that represents a current component that is being rendered. - */ -export function isHostComponentStandalone(lView: LView): boolean { - !ngDevMode && throwError('Must never be called in production mode'); - - const componentDef = getDeclarationComponentDef(lView); - // Treat host component as non-standalone if we can't obtain the def. - return !!(componentDef?.standalone); -} - -/** - * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`) - * and must **not** be used in production bundles. The function makes megamorphic reads, which might - * be too slow for production mode. - * - * Constructs a string describing the location of the host component template. The function is used - * in dev mode to produce error messages. - * - * @param lView An `LView` that represents a current component that is being rendered. - */ -export function getTemplateLocationDetails(lView: LView): string { - !ngDevMode && throwError('Must never be called in production mode'); - - const hostComponentDef = getDeclarationComponentDef(lView); - const componentClassName = hostComponentDef?.type?.name; - return componentClassName ? ` (used in the '${componentClassName}' component template)` : ''; -} - /** * When elements are created dynamically after a view blueprint is created (e.g. through * i18nApply()), we need to adjust the blueprint for future @@ -1089,10 +1015,8 @@ export function elementPropertyInternal( if (ngDevMode) { validateAgainstEventProperties(propName); - if (!validateProperty(element, tNode.value, propName, tView.schemas)) { - // Return here since we only log warnings for unknown properties. - handleUnknownPropertyError(propName, tNode, lView); - return; + if (!isPropertyValid(element, propName, tNode.value, tView.schemas)) { + handleUnknownPropertyError(propName, tNode.value, tNode.type, lView); } ngDevMode.rendererSetProperty++; } @@ -1110,7 +1034,7 @@ export function elementPropertyInternal( // 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, lView); + handleUnknownPropertyError(propName, tNode.value, tNode.type, lView); } } } @@ -1167,128 +1091,6 @@ export function setNgReflectProperties( } } -/** - * Validates that the property of the element is known at runtime and returns - * false if it's not the case. - * This check is relevant for JIT-compiled components (for AOT-compiled - * ones this check happens at build time). - * - * The property is considered known if either: - * - it's a known property of the element - * - the element is allowed by one of the schemas - * - the property is used for animations - * - * @param element Element to validate - * @param tagName Name of the tag to check - * @param propName Name of the property to check - * @param schemas Array of schemas - */ -function validateProperty( - element: RElement|RComment, tagName: string|null, propName: string, - schemas: SchemaMetadata[]|null): boolean { - // If `schemas` is set to `null`, that's an indication that this Component was compiled in AOT - // mode where this check happens at compile time. In JIT mode, `schemas` is always present and - // defined as an array (as an empty array in case `schemas` field is not defined) and we should - // execute the check below. - if (schemas === null) return true; - - // The property is considered valid if the element matches the schema, it exists on the element, - // or it is synthetic, and we are in a browser context (web worker nodes should be skipped). - if (matchingSchemas(schemas, tagName) || propName in element || isAnimationProp(propName)) { - return true; - } - - // Note: `typeof Node` returns 'function' in most browsers, but on IE it is 'object' so we - // need to account for both here, while being careful with `typeof null` also returning 'object'. - return typeof Node === 'undefined' || Node === null || !(element instanceof Node); -} - -/** - * Returns true if the tag name is allowed by specified schemas. - * @param schemas Array of schemas - * @param tagName Name of the tag - */ -export function matchingSchemas(schemas: SchemaMetadata[]|null, tagName: string|null): boolean { - if (schemas !== null) { - for (let i = 0; i < schemas.length; i++) { - const schema = schemas[i]; - if (schema === NO_ERRORS_SCHEMA || - schema === CUSTOM_ELEMENTS_SCHEMA && tagName && tagName.indexOf('-') > -1) { - return true; - } - } - } - - 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 tNode A `TNode` that represents a current component that is being rendered. - * @param lView An `LView` that represents a current component that is being rendered. - */ -function handleUnknownPropertyError(propName: string, tNode: TNode, lView: LView): void { - let tagName = tNode.value; - - // Special-case a situation when a structural directive is applied to - // an `` element, for example: ``. - // In this case the compiler generates the `ɵɵtemplate` instruction with - // the `null` as the tagName. The directive matching logic at runtime relies - // on this effect (see `isInlineTemplate`), thus using the 'ng-template' as - // a default value of the `tNode.value` is not feasible at this moment. - if (!tagName && tNode.type === TNodeType.Container) { - tagName = 'ng-template'; - } - - const isHostStandalone = isHostComponentStandalone(lView); - const templateLocation = getTemplateLocationDetails(lView); - - let message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'${ - templateLocation}.`; - - const schemas = `'${isHostStandalone ? '@Component' : '@NgModule'}.schemas'`; - const importLocation = isHostStandalone ? - '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 { - console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message)); - } -} - /** * Instantiate a root component. */ diff --git a/packages/core/test/acceptance/ng_module_spec.ts b/packages/core/test/acceptance/ng_module_spec.ts index 5d618363fd2..9faff9c125d 100644 --- a/packages/core/test/acceptance/ng_module_spec.ts +++ b/packages/core/test/acceptance/ng_module_spec.ts @@ -8,7 +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 {KNOWN_CONTROL_FLOW_DIRECTIVES} from '@angular/core/src/render3/instructions/element_validation'; import {TestBed} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';