mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): improve code around element validation (#46175)
improve code regarding element validation by creating a new file exporting validation functions and not exporting utils previously globally available PR Close #46175
This commit is contained in:
parent
5cfde8b23e
commit
d846bba678
5 changed files with 306 additions and 290 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
298
packages/core/src/render3/instructions/element_validation.ts
Normal file
298
packages/core/src/render3/instructions/element_validation.ts
Normal file
|
|
@ -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 `<ng-template>` element, for example: `<ng-template *ngIf="true">`.
|
||||
// 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<unknown>|null {
|
||||
!ngDevMode && throwError('Must never be called in production mode');
|
||||
|
||||
const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView<Type<unknown>>;
|
||||
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;
|
||||
}
|
||||
|
|
@ -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<unknown>|null {
|
||||
!ngDevMode && throwError('Must never be called in production mode');
|
||||
|
||||
const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView<Type<unknown>>;
|
||||
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<T>(
|
|||
|
||||
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<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, 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 `<ng-template>` element, for example: `<ng-template *ngIf="true">`.
|
||||
// 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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue