fix(ivy): compile queries in JIT mode (#26816)

PR Close #26816
This commit is contained in:
Pawel Kozlowski 2018-10-29 10:07:40 +01:00 committed by Matias Niemelä
parent 96770e5c6b
commit 578e4c7642
4 changed files with 184 additions and 31 deletions

View File

@ -217,8 +217,8 @@ export interface R3QueryMetadata {
descendants: boolean;
/**
* An expression representing a type to read from each matched node, or null if the node itself
* is to be returned.
* An expression representing a type to read from each matched node, or null if the default value
* for a given node is to be returned.
*/
read: o.Expression|null;
}

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler';
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler';
import {Query} from '../../metadata/di';
import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives';
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
import {ViewEncapsulation} from '../../metadata/view';
@ -69,14 +70,13 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
metadata.animations !== null ? new WrappedNodeExpr(metadata.animations) : null;
// Compile the component metadata, including template, into an expression.
// TODO(alxhub): implement inputs, outputs, queries, etc.
const res = compileR3Component(
{
...directiveMetadata(type, metadata),
template,
directives: new Map(),
pipes: new Map(),
viewQueries: [],
viewQueries: extractQueriesMetadata(getReflect().propMetadata(type), isViewQuery),
wrapDirectivesAndPipesInClosure: false,
styles: metadata.styles || [],
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, animations,
@ -176,7 +176,7 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
deps: reflectDependencies(type), host,
inputs: {...inputsFromMetadata, ...inputsFromType},
outputs: {...outputsFromMetadata, ...outputsFromType},
queries: [],
queries: extractQueriesMetadata(propMetadata, isContentQuery),
lifecycle: {
usesOnChanges: type.prototype.ngOnChanges !== undefined,
},
@ -215,6 +215,38 @@ function extractHostBindings(metadata: Directive, propMetadata: {[key: string]:
return {attributes, listeners, properties};
}
function convertToR3QueryPredicate(selector: any): Expression|string[] {
return typeof selector === 'string' ? splitByComma(selector) : new WrappedNodeExpr(selector);
}
export function convertToR3QueryMetadata(propertyName: string, ann: Query): R3QueryMetadata {
return {
propertyName: propertyName,
predicate: convertToR3QueryPredicate(ann.selector),
descendants: ann.descendants,
first: ann.first,
read: ann.read ? new WrappedNodeExpr(ann.read) : null
};
}
function extractQueriesMetadata(
propMetadata: {[key: string]: any[]},
isQueryAnn: (ann: any) => ann is Query): R3QueryMetadata[] {
const queriesMeta: R3QueryMetadata[] = [];
for (const field in propMetadata) {
if (propMetadata.hasOwnProperty(field)) {
propMetadata[field].forEach(ann => {
if (isQueryAnn(ann)) {
queriesMeta.push(convertToR3QueryMetadata(field, ann));
}
});
}
}
return queriesMeta;
}
function isInput(value: any): value is Input {
return value.ngMetadataName === 'Input';
}
@ -231,10 +263,24 @@ function isHostListener(value: any): value is HostListener {
return value.ngMetadataName === 'HostListener';
}
function isContentQuery(value: any): value is Query {
const name = value.ngMetadataName;
return name === 'ContentChild' || name === 'ContentChildren';
}
function isViewQuery(value: any): value is Query {
const name = value.ngMetadataName;
return name === 'ViewChild' || name === 'ViewChildren';
}
function splitByComma(value: string): string[] {
return value.split(',').map(piece => piece.trim());
}
function parseInputOutputs(values: string[]): StringMap {
return values.reduce(
(map, value) => {
const [field, property] = value.split(',').map(piece => piece.trim());
const [field, property] = splitByComma(value);
map[field] = property || field;
return map;
},

View File

@ -8,10 +8,12 @@
import 'reflect-metadata';
import {ElementRef, QueryList} from '@angular/core';
import {InjectorDef, defineInjectable} from '@angular/core/src/di/defs';
import {Injectable} from '@angular/core/src/di/injectable';
import {inject, setCurrentInjector} from '@angular/core/src/di/injector_compatibility';
import {ivyEnabled} from '@angular/core/src/ivy_switch';
import {ContentChild, ContentChildren, ViewChild, ViewChildren} from '@angular/core/src/metadata/di';
import {Component, Directive, HostBinding, HostListener, Input, Output, Pipe} from '@angular/core/src/metadata/directives';
import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module';
import {ComponentDef, PipeDef} from '@angular/core/src/render3/interfaces/definition';
@ -304,6 +306,54 @@ ivyEnabled && describe('render3 jit', () => {
expect((C as any).ngBaseDef).toBeDefined();
expect((C as any).ngBaseDef.outputs).toEqual({prop1: 'alias1', prop2: 'alias2'});
});
it('should compile ContentChildren query on a directive', () => {
@Directive({selector: '[test]'})
class TestDirective {
@ContentChildren('foo') foos: QueryList<ElementRef>|undefined;
}
expect((TestDirective as any).ngDirectiveDef.contentQueries).not.toBeNull();
expect((TestDirective as any).ngDirectiveDef.contentQueriesRefresh).not.toBeNull();
});
it('should compile ContentChild query on a directive', () => {
@Directive({selector: '[test]'})
class TestDirective {
@ContentChild('foo') foo: ElementRef|undefined;
}
expect((TestDirective as any).ngDirectiveDef.contentQueries).not.toBeNull();
expect((TestDirective as any).ngDirectiveDef.contentQueriesRefresh).not.toBeNull();
});
it('should not pick up view queries from directives', () => {
@Directive({selector: '[test]'})
class TestDirective {
@ViewChildren('foo') foos: QueryList<ElementRef>|undefined;
}
expect((TestDirective as any).ngDirectiveDef.contentQueries).toBeNull();
expect((TestDirective as any).ngDirectiveDef.viewQuery).toBeNull();
});
it('should compile ViewChild query on a component', () => {
@Component({selector: 'test', template: ''})
class TestComponent {
@ViewChild('foo') foo: ElementRef|undefined;
}
expect((TestComponent as any).ngComponentDef.foo).not.toBeNull();
});
it('should compile ViewChildren query on a component', () => {
@Component({selector: 'test', template: ''})
class TestComponent {
@ViewChildren('foo') foos: QueryList<ElementRef>|undefined;
}
expect((TestComponent as any).ngComponentDef.viewQuery).not.toBeNull();
});
});
it('ensure at least one spec exists', () => {});

View File

@ -6,34 +6,91 @@
* found in the LICENSE file at https://angular.io/license
*/
import {extendsDirectlyFromObject} from '../../../src/render3/jit/directive';
import {WrappedNodeExpr} from '@angular/compiler';
import {convertToR3QueryMetadata, extendsDirectlyFromObject} from '../../../src/render3/jit/directive';
describe('extendsDirectlyFromObject', () => {
it('should correctly behave with instanceof', () => {
expect(new Child() instanceof Object).toBeTruthy();
expect(new Child() instanceof Parent).toBeTruthy();
expect(new Parent() instanceof Child).toBeFalsy();
describe('jit directive helper functions', () => {
expect(new Child5() instanceof Object).toBeTruthy();
expect(new Child5() instanceof Parent5).toBeTruthy();
expect(new Parent5() instanceof Child5).toBeFalsy();
describe('extendsDirectlyFromObject', () => {
// Inheritance Example using Classes
class Parent {}
class Child extends Parent {}
// Inheritance Example using Function
const Parent5 = function Parent5() {} as any as{new (): {}};
const Child5 = function Child5() {} as any as{new (): {}};
Child5.prototype = new Parent5;
Child5.prototype.constructor = Child5;
it('should correctly behave with instanceof', () => {
expect(new Child() instanceof Object).toBeTruthy();
expect(new Child() instanceof Parent).toBeTruthy();
expect(new Parent() instanceof Child).toBeFalsy();
expect(new Child5() instanceof Object).toBeTruthy();
expect(new Child5() instanceof Parent5).toBeTruthy();
expect(new Parent5() instanceof Child5).toBeFalsy();
});
it('should detect direct inheritance form Object', () => {
expect(extendsDirectlyFromObject(Parent)).toBeTruthy();
expect(extendsDirectlyFromObject(Child)).toBeFalsy();
expect(extendsDirectlyFromObject(Parent5)).toBeTruthy();
expect(extendsDirectlyFromObject(Child5)).toBeFalsy();
});
});
it('should detect direct inheritance form Object', () => {
expect(extendsDirectlyFromObject(Parent)).toBeTruthy();
expect(extendsDirectlyFromObject(Child)).toBeFalsy();
describe('convertToR3QueryMetadata', () => {
it('should convert decorator with a single string selector', () => {
expect(convertToR3QueryMetadata('propName', {
selector: 'localRef',
descendants: false,
first: false,
isViewQuery: false,
read: undefined
})).toEqual({
propertyName: 'propName',
predicate: ['localRef'],
descendants: false,
first: false,
read: null
});
});
it('should convert decorator with multiple string selectors', () => {
expect(convertToR3QueryMetadata('propName', {
selector: 'foo, bar,baz',
descendants: true,
first: true,
isViewQuery: true,
read: undefined
})).toEqual({
propertyName: 'propName',
predicate: ['foo', 'bar', 'baz'],
descendants: true,
first: true,
read: null
});
});
it('should convert decorator with type selector and read option', () => {
class Directive {}
const converted = convertToR3QueryMetadata('propName', {
selector: Directive,
descendants: true,
first: true,
isViewQuery: true,
read: Directive
});
expect(converted.predicate).toEqual(jasmine.any(WrappedNodeExpr));
expect(converted.read).toEqual(jasmine.any(WrappedNodeExpr));
});
expect(extendsDirectlyFromObject(Parent5)).toBeTruthy();
expect(extendsDirectlyFromObject(Child5)).toBeFalsy();
});
});
// Inheritance Example using Classes
class Parent {}
class Child extends Parent {}
// Inheritance Example using Function
const Parent5 = function Parent5() {} as any as{new (): {}};
const Child5 = function Child5() {} as any as{new (): {}};
Child5.prototype = new Parent5;
Child5.prototype.constructor = Child5;