diff --git a/packages/compiler/src/output/abstract_js_emitter.ts b/packages/compiler/src/output/abstract_js_emitter.ts index 6a70b78ced..8dfa19bc3d 100644 --- a/packages/compiler/src/output/abstract_js_emitter.ts +++ b/packages/compiler/src/output/abstract_js_emitter.ts @@ -82,8 +82,11 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor { return null; } visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any { - ctx.print(stmt, `var ${stmt.name} = `); - stmt.value.visitExpression(this, ctx); + ctx.print(stmt, `var ${stmt.name}`); + if (stmt.value) { + ctx.print(stmt, ' = '); + stmt.value.visitExpression(this, ctx); + } ctx.println(stmt, `;`); return null; } diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index f5b930dce6..e7cf3644c3 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -756,14 +756,14 @@ export abstract class Statement { export class DeclareVarStmt extends Statement { public type: Type|null; constructor( - public name: string, public value: Expression, type?: Type|null, + public name: string, public value?: Expression, type?: Type|null, modifiers: StmtModifier[]|null = null, sourceSpan?: ParseSourceSpan|null) { super(modifiers, sourceSpan); - this.type = type || value.type; + this.type = type || (value && value.type) || null; } isEquivalent(stmt: Statement): boolean { return stmt instanceof DeclareVarStmt && this.name === stmt.name && - this.value.isEquivalent(stmt.value); + (this.value ? !!stmt.value && this.value.isEquivalent(stmt.value) : !stmt.value); } visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitDeclareVarStmt(this, context); @@ -1087,11 +1087,9 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor { } visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any { + const value = stmt.value && stmt.value.visitExpression(this, context); return this.transformStmt( - new DeclareVarStmt( - stmt.name, stmt.value.visitExpression(this, context), stmt.type, stmt.modifiers, - stmt.sourceSpan), - context); + new DeclareVarStmt(stmt.name, value, stmt.type, stmt.modifiers, stmt.sourceSpan), context); } visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any { return this.transformStmt( @@ -1275,7 +1273,9 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor } visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any { - stmt.value.visitExpression(this, context); + if (stmt.value) { + stmt.value.visitExpression(this, context); + } if (stmt.type) { stmt.type.visitType(this, context); } diff --git a/packages/compiler/src/output/output_interpreter.ts b/packages/compiler/src/output/output_interpreter.ts index 3f2b134a84..24747422e2 100644 --- a/packages/compiler/src/output/output_interpreter.ts +++ b/packages/compiler/src/output/output_interpreter.ts @@ -95,7 +95,8 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor { debugAst(ast: o.Expression|o.Statement|o.Type): string { return debugOutputAstAsTypeScript(ast); } visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: _ExecutionContext): any { - ctx.vars.set(stmt.name, stmt.value.visitExpression(this, ctx)); + const initialValue = stmt.value ? stmt.value.visitExpression(this, ctx) : undefined; + ctx.vars.set(stmt.name, initialValue); if (stmt.hasModifier(o.StmtModifier.Exported)) { ctx.exports.push(stmt.name); } diff --git a/packages/compiler/src/output/ts_emitter.ts b/packages/compiler/src/output/ts_emitter.ts index 54df9a3b36..ef96f62001 100644 --- a/packages/compiler/src/output/ts_emitter.ts +++ b/packages/compiler/src/output/ts_emitter.ts @@ -161,8 +161,10 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor } ctx.print(stmt, ` ${stmt.name}`); this._printColonType(stmt.type, ctx); - ctx.print(stmt, ` = `); - stmt.value.visitExpression(this, ctx); + if (stmt.value) { + ctx.print(stmt, ` = `); + stmt.value.visitExpression(this, ctx); + } ctx.println(stmt, `;`); return null; } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 253fdd1f63..6d239df39b 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -110,5 +110,8 @@ export class Identifiers { static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE}; + static query: o.ExternalReference = {name: 'ɵQ', moduleName: CORE}; + static queryRefresh: o.ExternalReference = {name: 'ɵqR', moduleName: CORE}; + static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; } \ No newline at end of file diff --git a/packages/compiler/src/render3/r3_pipe_compiler.ts b/packages/compiler/src/render3/r3_pipe_compiler.ts index 3593418efd..e0b189a755 100644 --- a/packages/compiler/src/render3/r3_pipe_compiler.ts +++ b/packages/compiler/src/render3/r3_pipe_compiler.ts @@ -24,7 +24,7 @@ export function compilePipe( {key: 'type', value: outputCtx.importExpr(pipe.type.reference), quoted: false}); // e.g. factory: function MyPipe_Factory() { return new MyPipe(); }, - const templateFactory = createFactory(pipe.type, outputCtx, reflector); + const templateFactory = createFactory(pipe.type, outputCtx, reflector, []); definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); // e.g. pure: true diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index 84df4f2319..9be0521d2c 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompilePipeSummary, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; import {BindingForm, BuiltinConverter, BuiltinFunctionCall, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../constant_pool'; @@ -47,7 +47,7 @@ export function compileDirective( {key: 'type', value: outputCtx.importExpr(directive.type.reference), quoted: false}); // e.g. `factory: () => new MyApp(injectElementRef())` - const templateFactory = createFactory(directive.type, outputCtx, reflector); + const templateFactory = createFactory(directive.type, outputCtx, reflector, directive.queries); definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); // e.g 'inputs: {a: 'a'}` @@ -108,9 +108,15 @@ export function compileComponent( } // e.g. `factory: function MyApp_Factory() { return new MyApp(injectElementRef()); }` - const templateFactory = createFactory(component.type, outputCtx, reflector); + const templateFactory = createFactory(component.type, outputCtx, reflector, component.queries); definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + // e.g `hostBindings: function MyApp_HostBindings { ... } + const hostBindings = createHostBindingsFunction(component.type, outputCtx, component.queries); + if (hostBindings) { + definitionMapValues.push({key: 'hostBindings', value: hostBindings, quoted: false}); + } + // e.g. `template: function MyComponent_Template(_ctx, _cm) {...}` const templateTypeName = component.type.reference.name; const templateName = templateTypeName ? `${templateTypeName}_Template` : null; @@ -118,7 +124,8 @@ export function compileComponent( const templateFunctionExpression = new TemplateDefinitionBuilder( outputCtx, outputCtx.constantPool, reflector, CONTEXT_NAME, ROOT_SCOPE.nestedScope(), 0, - component.template !.ngContentSelectors, templateTypeName, templateName, pipeMap) + component.template !.ngContentSelectors, templateTypeName, templateName, pipeMap, + component.viewQueries) .buildTemplateFunction(template, []); definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); @@ -296,7 +303,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private reflector: CompileReflector, private contextParameter: string, private bindingScope: BindingScope, private level = 0, private ngContentSelectors: string[], private contextName: string|null, private templateName: string|null, - private pipes: Map) { + private pipes: Map, private viewQueries: CompileQueryMetadata[]) { this._valueConverter = new ValueConverter( outputCtx, () => this.allocateDataSlot(), (name, localName, slot, value) => { bindingScope.set(localName, value); @@ -353,6 +360,32 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { } } + // Define and update any view queries + for (let query of this.viewQueries) { + // e.g. r3.Q(0, SomeDirective, true); + const querySlot = this.allocateDataSlot(); + const predicate = getQueryPredicate(query, this.outputCtx); + const args = [ + /* memoryIndex */ o.literal(querySlot, o.INFERRED_TYPE), + /* predicate */ predicate, + /* descend */ o.literal(query.descendants, o.INFERRED_TYPE) + ]; + + if (query.read) { + args.push(this.outputCtx.importExpr(query.read.identifier !.reference)); + } + this.instruction(this._creationMode, null, R3.query, ...args); + + // (r3.qR(tmp = r3.ɵld(0)) && (ctx.someDir = tmp)); + const temporary = this.temp(); + const getQueryList = o.importExpr(R3.load).callFn([o.literal(querySlot)]); + const refresh = o.importExpr(R3.queryRefresh).callFn([temporary.set(getQueryList)]); + const updateDirective = o.variable(CONTEXT_NAME) + .prop(query.propertyName) + .set(query.first ? temporary.prop('first') : temporary); + this._bindingMode.push(refresh.and(updateDirective).toStmt()); + } + templateVisitAll(this, asts); const creationMode = this._creationMode.length > 0 ? @@ -589,7 +622,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { const templateVisitor = new TemplateDefinitionBuilder( this.outputCtx, this.constantPool, this.reflector, templateContext, this.bindingScope.nestedScope(), this.level + 1, this.ngContentSelectors, contextName, - templateName, this.pipes); + templateName, this.pipes, []); const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children, ast.variables); this._postfix.push(templateFunctionExpr.toDeclStmt(templateName, null)); } @@ -643,9 +676,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private temp(): o.ReadVarExpr { if (!this._temporaryAllocated) { - this._prefix.push(o.variable(TEMPORARY_NAME, o.DYNAMIC_TYPE,  null) - .set(o.literal(undefined)) - .toDeclStmt(o.DYNAMIC_TYPE)); + this._prefix.push(new o.DeclareVarStmt(TEMPORARY_NAME, undefined, o.DYNAMIC_TYPE)); this._temporaryAllocated = true; } return o.variable(TEMPORARY_NAME); @@ -665,9 +696,31 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { } } +function getQueryPredicate(query: CompileQueryMetadata, outputCtx: OutputContext): o.Expression { + let predicate: o.Expression; + if (query.selectors.length > 1 || (query.selectors.length == 1 && query.selectors[0].value)) { + const selectors = query.selectors.map(value => value.value as string); + selectors.some(value => !value) && error('Found a type among the string selectors expected'); + predicate = outputCtx.constantPool.getConstLiteral( + o.literalArr(selectors.map(value => o.literal(value)))); + } else if (query.selectors.length == 1) { + const first = query.selectors[0]; + if (first.identifier) { + predicate = outputCtx.importExpr(first.identifier.reference); + } else { + error('Unexpected query form'); + predicate = o.literal(null); + } + } else { + error('Unexpected query form'); + predicate = o.literal(null); + } + return predicate; +} + export function createFactory( - type: CompileTypeMetadata, outputCtx: OutputContext, - reflector: CompileReflector): o.FunctionExpr { + type: CompileTypeMetadata, outputCtx: OutputContext, reflector: CompileReflector, + queries: CompileQueryMetadata[]): o.Expression { let args: o.Expression[] = []; const elementRef = reflector.resolveExternalReference(Identifiers.ElementRef); @@ -700,10 +753,74 @@ export function createFactory( } } + const queryDefinitions: o.Expression[] = []; + for (let query of queries) { + const predicate = getQueryPredicate(query, outputCtx); + + // e.g. r3.Q(null, SomeDirective, false) or r3.Q(null, ['div'], false) + const parameters = [ + /* memoryIndex */ o.literal(null, o.INFERRED_TYPE), + /* predicate */ predicate, + /* descend */ o.literal(query.descendants) + ]; + + if (query.read) { + parameters.push(outputCtx.importExpr(query.read.identifier !.reference)); + } + + queryDefinitions.push(o.importExpr(R3.query).callFn(parameters)); + } + + const createInstance = new o.InstantiateExpr(outputCtx.importExpr(type.reference), args); + const result = queryDefinitions.length > 0 ? o.literalArr([createInstance, ...queryDefinitions]) : + createInstance; + return o.fn( - [], - [new o.ReturnStatement(new o.InstantiateExpr(outputCtx.importExpr(type.reference), args))], - o.INFERRED_TYPE, null, type.reference.name ? `${type.reference.name}_Factory` : null); + [], [new o.ReturnStatement(result)], o.INFERRED_TYPE, null, + type.reference.name ? `${type.reference.name}_Factory` : null); +} + +// Return a host binding function or null if one is not necessary. +export function createHostBindingsFunction( + type: CompileTypeMetadata, outputCtx: OutputContext, + queries: CompileQueryMetadata[]): o.Expression|null { + const statements: o.Statement[] = []; + + const temporary = function() { + let declared = false; + return () => { + if (!declared) { + statements.push(new o.DeclareVarStmt(TEMPORARY_NAME, undefined, o.DYNAMIC_TYPE)); + declared = true; + } + return o.variable(TEMPORARY_NAME); + }; + }(); + + for (let index = 0; index < queries.length; index++) { + const query = queries[index]; + + // e.g. r3.qR(tmp = r3.ld(dirIndex)[1]) && (r3.ld(dirIndex)[0].someDir = tmp); + const getDirectiveMemory = o.importExpr(R3.load).callFn([o.variable('dirIndex')]); + // The query list is at the query index + 1 because the directive itself is in slot 0. + const getQueryList = getDirectiveMemory.key(o.literal(index + 1)); + const assignToTemporary = temporary().set(getQueryList); + const callQueryRefresh = o.importExpr(R3.queryRefresh).callFn([assignToTemporary]); + const updateDirective = getDirectiveMemory.key(o.literal(0, o.INFERRED_TYPE)) + .prop(query.propertyName) + .set(query.first ? temporary().key(o.literal(0)) : temporary()); + const andExpression = callQueryRefresh.and(updateDirective); + statements.push(andExpression.toStmt()); + } + + if (statements.length > 0) { + return o.fn( + [new o.FnParam('dirIndex', o.NUMBER_TYPE), new o.FnParam('elIndex', o.NUMBER_TYPE)], + statements, o.INFERRED_TYPE, null, + type.reference.name ? `${type.reference.name}_HostBindings` : null); + } + + return null; } class ValueConverter extends AstMemoryEfficientTransformer { diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index f2149d2f48..c675688a2f 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -551,6 +551,133 @@ describe('compiler compliance', () => { result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); }); + describe('queries', () => { + const directive = { + 'some.directive.ts': ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[someDir]', + }) + export class SomeDirective { } + ` + }; + + it('should support view queries', () => { + const files = { + app: { + ...directive, + 'view_query.component.ts': ` + import {Component, NgModule, ViewChild} from '@angular/core'; + import {SomeDirective} from './some.directive'; + + @Component({ + selector: 'view-query-component', + template: \` +
+ \` + }) + export class ViewQueryComponent { + @ViewChild(SomeDirective) someDir: SomeDirective; + } + + @NgModule({declarations: [SomeDirective, ViewQueryComponent]}) + export class MyModule {} + ` + } + }; + + const ViewQueryComponentDefinition = ` + const $e0_attrs$ = ['someDir','']; + const $e1_dirs$ = [SomeDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: ViewQueryComponent, + tag: 'view-query-component', + factory: function ViewQueryComponent_Factory() { return new ViewQueryComponent(); }, + template: function ViewQueryComponent_Template(ctx: $ViewQueryComponent$, cm: $boolean$) { + var $tmp$: $any$; + if (cm) { + $r3$.ɵQ(0, SomeDirective, true); + $r3$.ɵE(1, 'div', $e0_attrs$, $e1_dirs$); + $r3$.ɵe(); + } + ($r3$.ɵqR(($tmp$ = $r3$.ɵld(0))) && (ctx.someDir = $tmp$.first)); + SomeDirective.ngDirectiveDef.h(2, 1); + $r3$.ɵr(2, 1); + } + });`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, ViewQueryComponentDefinition, 'Invalid ViewQuery declaration'); + }); + + it('should support content queries', () => { + const files = { + app: { + ...directive, + 'spec.ts': ` + import {Component, ContentChild, NgModule} from '@angular/core'; + import {SomeDirective} from './some.directive'; + + @Component({ + selector: 'content-query-component', + template: \` +
+ \` + }) + export class ContentQueryComponent { + @ContentChild(SomeDirective) someDir: SomeDirective; + } + + @Component({ + selector: 'my-app', + template: \` + +
+
+ \` + }) + export class MyApp { } + + @NgModule({declarations: [SomeDirective, ContentQueryComponent, MyApp]}) + export class MyModule { } + ` + } + }; + + const ContentQueryComponentDefinition = ` + static ngComponentDef = $r3$.ɵdefineComponent({ + type: ContentQueryComponent, + tag: 'content-query-component', + factory: function ContentQueryComponent_Factory() { + return [new ContentQueryComponent(), $r3$.ɵQ(null, SomeDirective, true)]; + }, + hostBindings: function ContentQueryComponent_HostBindings( + dirIndex: $number$, elIndex: $number$) { + var $tmp$: $any$; + ($r3$.ɵqR(($tmp$ = $r3$.ɵld(dirIndex)[1])) && ($r3$.ɵld(dirIndex)[0].someDir = $tmp$[0])); + }, + template: function ContentQueryComponent_Template( + ctx: $ContentQueryComponent$, cm: $boolean$) { + if (cm) { + $r3$.ɵpD(0); + $r3$.ɵE(1, 'div'); + $r3$.ɵP(2, 0); + $r3$.ɵe(); + } + } + });`; + + const result = compile(files, angularFiles); + + const source = result.source; + expectEmit(source, ContentQueryComponentDefinition, 'Invalid ContentQuery declaration'); + }); + }); + describe('pipes', () => { const files = {