diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index b64abc6716..f3067a6c0e 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -85,5 +85,5 @@ export {jitExpression} from './render3/r3_jit'; export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory'; export {compileNgModule, R3NgModuleMetadata} from './render3/r3_module_compiler'; export {makeBindingParser, parseTemplate} from './render3/view/template'; -export {compileComponentFromMetadata, compileDirectiveFromMetadata} from './render3/view/compiler'; +export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler'; // This file only reexports content of the `src` folder. Keep it that way. \ No newline at end of file diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index d830630418..089b652a7c 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -445,3 +445,45 @@ function typeMapToExpressionMap( ([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]); return new Map(entries); } + +const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/; + +// Represents the groups in the above regex. +const enum HostBindingGroup { + // group 1: "prop" from "[prop]" + Property = 1, + + // group 2: "event" from "(event)" + Event = 2, + + // group 3: "@trigger" from "@trigger" + Animation = 3, +} + +export function parseHostBindings(host: {[key: string]: string}): { + attributes: {[key: string]: string}, + listeners: {[key: string]: string}, + properties: {[key: string]: string}, + animations: {[key: string]: string}, +} { + const attributes: {[key: string]: string} = {}; + const listeners: {[key: string]: string} = {}; + const properties: {[key: string]: string} = {}; + const animations: {[key: string]: string} = {}; + + Object.keys(host).forEach(key => { + const value = host[key]; + const matches = key.match(HOST_REG_EXP); + if (matches === null) { + attributes[key] = value; + } else if (matches[HostBindingGroup.Property] != null) { + properties[matches[HostBindingGroup.Property]] = value; + } else if (matches[HostBindingGroup.Event] != null) { + listeners[matches[HostBindingGroup.Event]] = value; + } else if (matches[HostBindingGroup.Animation] != null) { + animations[matches[HostBindingGroup.Animation]] = value; + } + }); + + return {attributes, listeners, properties, animations}; +} \ No newline at end of file diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 04aa7ba9f7..44d6c0951a 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -221,7 +221,7 @@ export class BindingParser { private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan): ASTWithSource { - const sourceInfo = sourceSpan.start.toString(); + const sourceInfo = (sourceSpan && sourceSpan.start || '(unknown)').toString(); try { const ast = isHostBinding ? @@ -343,7 +343,7 @@ export class BindingParser { } private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { - const sourceInfo = sourceSpan.start.toString(); + const sourceInfo = (sourceSpan && sourceSpan.start || '(unknown').toString(); try { const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig); diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 9e5d00539d..9c0b2d3fb1 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseTemplate} from '@angular/compiler'; +import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler'; -import {Component, Directive, HostBinding, Input, Output} from '../../metadata/directives'; +import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives'; import {ReflectionCapabilities} from '../../reflection/reflection_capabilities'; import {Type} from '../../type'; @@ -19,6 +19,10 @@ import {getReflect, reflectDependencies} from './util'; let _pendingPromises: Promise[] = []; +type StringMap = { + [key: string]: string +}; + /** * Compile an Angular component according to its decorator metadata, and patch the resulting * ngComponentDef onto the component type. @@ -137,12 +141,14 @@ export function awaitCurrentlyCompilingComponents(): Promise { */ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMetadata { // Reflect inputs and outputs. - const props = getReflect().propMetadata(type); - const inputs: {[key: string]: string} = {}; - const outputs: {[key: string]: string} = {}; + const propMetadata = getReflect().propMetadata(type); + const inputs: StringMap = {}; + const outputs: StringMap = {}; - for (let field in props) { - props[field].forEach(ann => { + const host = extractHostBindings(metadata, propMetadata); + + for (let field in propMetadata) { + propMetadata[field].forEach(ann => { if (isInput(ann)) { inputs[field] = ann.bindingPropertyName || field; } else if (isOutput(ann)) { @@ -155,14 +161,7 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet name: type.name, type: new WrappedNodeExpr(type), selector: metadata.selector !, - deps: reflectDependencies(type), - host: { - attributes: {}, - listeners: {}, - properties: {}, - }, - inputs, - outputs, + deps: reflectDependencies(type), host, inputs, outputs, queries: [], lifecycle: { usesOnChanges: type.prototype.ngOnChanges !== undefined, @@ -171,6 +170,32 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet }; } +function extractHostBindings(metadata: Directive, propMetadata: {[key: string]: any[]}): { + attributes: StringMap, + listeners: StringMap, + properties: StringMap, +} { + // First parse the declarations from the metadata. + const {attributes, listeners, properties, animations} = parseHostBindings(metadata.host || {}); + + if (Object.keys(animations).length > 0) { + throw new Error(`Animation bindings are as-of-yet unsupported in Ivy`); + } + + // Next, loop over the properties of the object, looking for @HostBinding and @HostListener. + for (let field in propMetadata) { + propMetadata[field].forEach(ann => { + if (isHostBinding(ann)) { + properties[ann.hostPropertyName || field] = field; + } else if (isHostListener(ann)) { + listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`; + } + }); + } + + return {attributes, listeners, properties}; +} + function isInput(value: any): value is Input { return value.ngMetadataName === 'Input'; } @@ -178,3 +203,11 @@ function isInput(value: any): value is Input { function isOutput(value: any): value is Output { return value.ngMetadataName === 'Output'; } + +function isHostBinding(value: any): value is HostBinding { + return value.ngMetadataName === 'HostBinding'; +} + +function isHostListener(value: any): value is HostListener { + return value.ngMetadataName === 'HostListener'; +} diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index f09edbe042..1a82a60372 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -9,7 +9,7 @@ import {Injectable} from '@angular/core/src/di/injectable'; import {inject, setCurrentInjector} from '@angular/core/src/di/injector'; import {ivyEnabled} from '@angular/core/src/ivy_switch'; -import {Component} from '@angular/core/src/metadata/directives'; +import {Component, HostBinding, HostListener} from '@angular/core/src/metadata/directives'; import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module'; import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; @@ -161,6 +161,29 @@ ivyEnabled && describe('render3 jit', () => { expect(cmpDef.directiveDefs instanceof Function).toBe(true); expect((cmpDef.directiveDefs as Function)()).toEqual([cmpDef]); }); + + it('should add hostbindings and hostlisteners', () => { + @Component({ + template: 'foo', + selector: 'foo', + host: { + '[class.red]': 'isRed', + '(click)': 'onClick()', + }, + }) + class Cmp { + @HostBinding('class.green') + green: boolean = false; + + @HostListener('change', ['$event']) + onChange(event: any): void {} + } + + const cmpDef = (Cmp as any).ngComponentDef as ComponentDef; + + expect(cmpDef.hostBindings).toBeDefined(); + expect(cmpDef.hostBindings !.length).toBe(2); + }); }); it('ensure at least one spec exists', () => {});