fix(core): prevent unknown property check for AOT-compiled components (#36072)

Prior to this commit, the unknown property check was unnecessarily invoked for AOT-compiled components (for these components, the check happens at compile time). This commit updates the code to avoid unknown property verification for AOT-compiled components by checking whether schemas information is present (as a way to detect whether this is JIT or AOT compiled component).

Resolves #35945.

PR Close #36072
This commit is contained in:
Andrew Kushnir 2020-03-15 09:08:57 -07:00 committed by atscott
parent 88b0985bad
commit 4a9f0bebc3
2 changed files with 120 additions and 65 deletions

View File

@ -1038,6 +1038,12 @@ export function setNgReflectProperties(
function validateProperty(
tView: TView, lView: LView, element: RElement|RComment, propName: string,
tNode: TNode): 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 (tView.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(tView, lView, tNode.tagName) || propName in element ||

View File

@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
import {Component, CUSTOM_ELEMENTS_SCHEMA, Injectable, InjectionToken, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelement as element} from '@angular/core';
import {Component, CUSTOM_ELEMENTS_SCHEMA, Injectable, InjectionToken, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelement as element, ɵɵproperty as property} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {modifiedInIvy, onlyInIvy} from '@angular/private/testing';
@ -247,73 +247,122 @@ describe('NgModule', () => {
}).toThrowError(/'custom' is not a known element/);
});
onlyInIvy('test relies on Ivy-specific AOT format')
.it('should not log unknown element warning for AOT-compiled components', () => {
const spy = spyOn(console, 'warn');
/*
* @Component({
* selector: 'comp',
* template: '<custom-el></custom-el>',
* })
* class MyComp {}
*/
class MyComp {
static ɵfac = () => new MyComp();
static ɵcmp = defineComponent({
type: MyComp,
selectors: [['comp']],
decls: 1,
vars: 0,
template:
function MyComp_Template(rf, ctx) {
if (rf & 1) {
element(0, 'custom-el');
}
},
encapsulation: 2
});
}
setClassMetadata(
MyComp, [{
type: Component,
args: [{
selector: 'comp',
template: '<custom-el></custom-el>',
}]
}],
null, null);
/*
* @NgModule({
* declarations: [MyComp],
* schemas: [NO_ERRORS_SCHEMA],
* })
* class MyModule {}
*/
class MyModule {
static ɵmod = defineNgModule({type: MyModule});
static ɵinj = defineInjector({factory: () => new MyModule()});
}
setClassMetadata(
MyModule, [{
type: NgModule,
args: [{
declarations: [MyComp],
schemas: [NO_ERRORS_SCHEMA],
}]
}],
null, null);
TestBed.configureTestingModule({
imports: [MyModule],
onlyInIvy('test relies on Ivy-specific AOT format').describe('AOT-compiled components', () => {
function createComponent(
template: (rf: any) => void, vars: number, consts?: (number|string)[][]) {
class Comp {
static ɵfac = () => new Comp();
static ɵcmp = defineComponent({
type: Comp,
selectors: [['comp']],
decls: 1,
vars,
consts,
template,
encapsulation: 2
});
}
setClassMetadata(
Comp, [{
type: Component,
args: [
{selector: 'comp', template: '...'},
]
}],
null, null);
return Comp;
}
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(spy).not.toHaveBeenCalled();
function createNgModule(Comp: any) {
class Module {
static ɵmod = defineNgModule({type: Module});
static ɵinj = defineInjector({factory: () => new Module()});
}
setClassMetadata(
Module, [{
type: NgModule,
args: [{
declarations: [Comp],
schemas: [NO_ERRORS_SCHEMA],
}]
}],
null, null);
return Module;
}
it('should not log unknown element warning for AOT-compiled components', () => {
const spy = spyOn(console, 'warn');
/*
* @Component({
* selector: 'comp',
* template: '<custom-el></custom-el>',
* })
* class MyComp {}
*/
const MyComp = createComponent((rf: any) => {
if (rf & 1) {
element(0, 'custom-el');
}
}, 0);
/*
* @NgModule({
* declarations: [MyComp],
* schemas: [NO_ERRORS_SCHEMA],
* })
* class MyModule {}
*/
const MyModule = createNgModule(MyComp);
TestBed.configureTestingModule({
imports: [MyModule],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(spy).not.toHaveBeenCalled();
});
it('should not log unknown property warning for AOT-compiled components', () => {
const spy = spyOn(console, 'warn');
/*
* @Component({
* selector: 'comp',
* template: '<div [foo]="1"></div>',
* })
* class MyComp {}
*/
const MyComp = createComponent((rf: any) => {
if (rf & 1) {
element(0, 'div', 0);
}
if (rf & 2) {
property('foo', true);
}
}, 1, [[3, 'foo']]);
/*
* @NgModule({
* declarations: [MyComp],
* schemas: [NO_ERRORS_SCHEMA],
* })
* class MyModule {}
*/
const MyModule = createNgModule(MyComp);
TestBed.configureTestingModule({
imports: [MyModule],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(spy).not.toHaveBeenCalled();
});
});
onlyInIvy('unknown element check logs a warning rather than throwing')
.it('should not warn about unknown elements with CUSTOM_ELEMENTS_SCHEMA', () => {
@Component({template: `<custom-el></custom-el>`})
@ -519,4 +568,4 @@ describe('NgModule', () => {
TestBed.createComponent(TestCmp);
expect(componentInstance).toBeAnInstanceOf(TestCmp);
});
});
});