From 578e4c764255b2944a97f761ea1516541c9b8aed Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Mon, 29 Oct 2018 10:07:40 +0100 Subject: [PATCH] fix(ivy): compile queries in JIT mode (#26816) PR Close #26816 --- packages/compiler/src/render3/view/api.ts | 4 +- packages/core/src/render3/jit/directive.ts | 56 +++++++++- packages/core/test/render3/ivy/jit_spec.ts | 50 +++++++++ .../core/test/render3/jit/directive_spec.ts | 105 ++++++++++++++---- 4 files changed, 184 insertions(+), 31 deletions(-) diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 192135f73b..57ab37e1d8 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -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; } diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 76cf70d0cd..17a42c748a 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -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, 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, 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; }, diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index 382b571f2a..21e9bfb2d5 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -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|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|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|undefined; + } + + expect((TestComponent as any).ngComponentDef.viewQuery).not.toBeNull(); + }); }); it('ensure at least one spec exists', () => {}); diff --git a/packages/core/test/render3/jit/directive_spec.ts b/packages/core/test/render3/jit/directive_spec.ts index 85c50a613a..01d055c3c7 100644 --- a/packages/core/test/render3/jit/directive_spec.ts +++ b/packages/core/test/render3/jit/directive_spec.ts @@ -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;