diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 164b729d76..e83a36c7b8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -434,7 +434,7 @@ function extractHostBindings( }); } - const {attributes, listeners, properties, animations} = parseHostBindings(hostMetadata); + const {attributes, listeners, properties} = parseHostBindings(hostMetadata); filterToMembersWithDecorator(members, 'HostBinding', coreModule) .forEach(({member, decorators}) => { diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index b30455e41c..7ad7b35f18 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -434,14 +434,14 @@ describe('compiler compliance', () => { $r3$.ɵallocHostVars(14); } if (rf & 2) { - $r3$.ɵelementProperty(elIndex, "expansionHeight", + $r3$.ɵcomponentHostSyntheticProperty(elIndex, "@expansionHeight", $r3$.ɵbind( $r3$.ɵpureFunction2(5, $_c1$, ctx.getExpandedState(), $r3$.ɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight) ) ), null, true ); - $r3$.ɵelementProperty(elIndex, "expansionWidth", + $r3$.ɵcomponentHostSyntheticProperty(elIndex, "@expansionWidth", $r3$.ɵbind( $r3$.ɵpureFunction2(11, $_c1$, ctx.getExpandedState(), $r3$.ɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth) diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts index 6de3d5585d..98dfb40fcd 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts @@ -173,7 +173,7 @@ describe('r3_view_compiler', () => { }; const template = ` - const _c0 = [3, "mySelector"]; + const _c0 = [3, "@mySelector"]; // ... template: function MyApp_Template(rf, ctx) { if (rf & 1) { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index a27da6d1fe..3df9f80d8e 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -281,8 +281,8 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵelementStart(0, "div", _c0); - $r3$.ɵlistener("@myAnimation.start", function MyComponent_Template_div__myAnimation_start_0_listener($event) { return ctx.onStart($event); }); - $r3$.ɵlistener("@myAnimation.done", function MyComponent_Template_div__myAnimation_done_0_listener($event) { return ctx.onDone($event); }); + $r3$.ɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_myAnimation_start_0_listener($event) { return ctx.onStart($event); }); + $r3$.ɵlistener("@myAnimation.done", function MyComponent_Template_div_animation_myAnimation_done_0_listener($event) { return ctx.onDone($event); }); $r3$.ɵelementEnd(); } if (rf & 2) { $r3$.ɵelementProperty(0, "@myAnimation", $r3$.ɵbind(ctx.exp)); @@ -296,6 +296,64 @@ describe('compiler compliance: styling', () => { const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); }); + + it('should generate animation host binding and listener code for directives', () => { + const files = { + app: { + 'spec.ts': ` + import {Directive, Component, NgModule} from '@angular/core'; + + @Directive({ + selector: '[my-anim-dir]', + animations: [ + {name: 'myAnim'} + ], + host: { + '[@myAnim]': 'myAnimState', + '(@myAnim.start)': 'onStart()', + '(@myAnim.done)': 'onDone()' + } + }) + class MyAnimDir { + onStart() {} + onDone() {} + myAnimState = '123'; + } + + @Component({ + selector: 'my-cmp', + template: \` +
+ \` + }) + class MyComponent { + } + + @NgModule({declarations: [MyComponent, MyAnimDir]}) + export class MyModule {} + ` + } + }; + + const template = ` + MyAnimDir.ngDirectiveDef = $r3$.ɵdefineDirective({ + … + hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵallocHostVars(1); + $r3$.ɵlistener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); }); + $r3$.ɵlistener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); }); + } if (rf & 2) { + $r3$.ɵcomponentHostSyntheticProperty(elIndex, "@myAnim", $r3$.ɵbind(ctx.myAnimState), null, true); + } + } + … + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); }); describe('[style] and [style.prop]', () => { diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 717efa833d..c79ba07621 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -250,11 +250,7 @@ function extractHostBindings(host: {[key: string]: string}, propMetadata: {[key: properties: StringMap, } { // First parse the declarations from the metadata. - const {attributes, listeners, properties, animations} = parseHostBindings(host || {}); - - if (Object.keys(animations).length > 0) { - throw new Error(`Animation bindings are as-of-yet unsupported in Ivy`); - } + const {attributes, listeners, properties} = parseHostBindings(host || {}); // Next, loop over the properties of the object, looking for @HostBinding and @HostListener. for (const field in propMetadata) { diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 33422adde6..90e33da888 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -31,6 +31,9 @@ export class Identifiers { static elementProperty: o.ExternalReference = {name: 'ɵelementProperty', moduleName: CORE}; + static componentHostSyntheticProperty: + o.ExternalReference = {name: 'ɵcomponentHostSyntheticProperty', moduleName: CORE}; + static elementAttribute: o.ExternalReference = {name: 'ɵelementAttribute', moduleName: CORE}; static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE}; diff --git a/packages/compiler/src/render3/util.ts b/packages/compiler/src/render3/util.ts index c006c298f5..493ed0e90e 100644 --- a/packages/compiler/src/render3/util.ts +++ b/packages/compiler/src/render3/util.ts @@ -52,3 +52,31 @@ export interface R3Reference { value: o.Expression; type: o.Expression; } + +const ANIMATE_SYMBOL_PREFIX = '@'; +export function prepareSyntheticPropertyName(name: string) { + return `${ANIMATE_SYMBOL_PREFIX}${name}`; +} + +export function prepareSyntheticListenerName(name: string, phase: string) { + return `${ANIMATE_SYMBOL_PREFIX}${name}.${phase}`; +} + +export function isSyntheticPropertyOrListener(name: string) { + return name.charAt(0) == ANIMATE_SYMBOL_PREFIX; +} + +export function getSyntheticPropertyName(name: string) { + // this will strip out listener phase values... + // @foo.start => @foo + const i = name.indexOf('.'); + name = i > 0 ? name.substring(0, i) : name; + if (name.charAt(0) !== ANIMATE_SYMBOL_PREFIX) { + name = ANIMATE_SYMBOL_PREFIX + name; + } + return name; +} + +export function prepareSyntheticListenerFunctionName(name: string, phase: string) { + return `animation_${name}_${phase}`; +} \ 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 9b37d76c2b..201e98e212 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -12,7 +12,7 @@ import {CompileReflector} from '../../compile_reflector'; import {BindingForm, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../../constant_pool'; import * as core from '../../core'; -import {AST, ParsedEvent} from '../../expression_parser/ast'; +import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast'; import {LifecycleHooks} from '../../lifecycle_reflector'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; @@ -25,7 +25,7 @@ import {OutputContext, error} from '../../util'; import {compileFactoryFunction, dependenciesFromGlobalMetadata} from '../r3_factory'; import {Identifiers as R3} from '../r3_identifiers'; import {Render3ParseResult} from '../r3_template_transform'; -import {typeWithParameters} from '../util'; +import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName, typeWithParameters} from '../util'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; import {StylingBuilder, StylingInstruction} from './styling_builder'; @@ -697,7 +697,7 @@ function createHostBindingsFunction( const value = binding.expression.visit(valueConverter); const bindingExpr = bindingFn(bindingContext, value); - const {bindingName, instruction, extraParams} = getBindingNameAndInstruction(name); + const {bindingName, instruction, extraParams} = getBindingNameAndInstruction(binding); const instructionParams: o.Expression[] = [ elVarExp, o.literal(bindingName), o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]) @@ -775,8 +775,9 @@ function createStylingStmt( .toStmt(); } -function getBindingNameAndInstruction(bindingName: string): +function getBindingNameAndInstruction(binding: ParsedProperty): {bindingName: string, instruction: o.ExternalReference, extraParams: o.Expression[]} { + let bindingName = binding.name; let instruction !: o.ExternalReference; const extraParams: o.Expression[] = []; @@ -786,7 +787,15 @@ function getBindingNameAndInstruction(bindingName: string): bindingName = attrMatches[1]; instruction = R3.elementAttribute; } else { - instruction = R3.elementProperty; + if (binding.isAnimation) { + bindingName = prepareSyntheticPropertyName(bindingName); + // host bindings that have a synthetic property (e.g. @foo) should always be rendered + // in the context of the component and not the parent. Therefore there is a special + // compatibility instruction available for this purpose. + instruction = R3.componentHostSyntheticProperty; + } else { + instruction = R3.elementProperty; + } extraParams.push( o.literal(null), // TODO: This should be a sanitizer fn (FW-785) o.literal(true) // host bindings must have nativeOnly prop set to true @@ -802,14 +811,19 @@ function createHostListeners( return eventBindings.map(binding => { const bindingExpr = convertActionBinding( null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation')); - const bindingName = binding.name && sanitizeIdentifier(binding.name); + let bindingName = binding.name && sanitizeIdentifier(binding.name); + let bindingFnName = bindingName; + if (binding.type === ParsedEventType.Animation) { + bindingFnName = prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase); + bindingName = prepareSyntheticListenerName(bindingName, binding.targetOrPhase); + } const typeName = meta.name; const functionName = - typeName && bindingName ? `${typeName}_${bindingName}_HostBindingHandler` : null; + typeName && bindingName ? `${typeName}_${bindingFnName}_HostBindingHandler` : null; const handler = o.fn( [new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts], o.INFERRED_TYPE, null, functionName); - return o.importExpr(R3.listener).callFn([o.literal(binding.name), handler]).toStmt(); + return o.importExpr(R3.listener).callFn([o.literal(bindingName), handler]).toStmt(); }); } @@ -832,30 +846,24 @@ function typeMapToExpressionMap( return new Map(entries); } -const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/; - +const HOST_REG_EXP = /^(?:\[([^\]]+)\])|(?:\(([^\)]+)\))$/; // Represents the groups in the above regex. const enum HostBindingGroup { - // group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]" + // group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]", or @anim from [@anim] Binding = 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]; @@ -863,15 +871,16 @@ export function parseHostBindings(host: {[key: string]: string}): { if (matches === null) { attributes[key] = value; } else if (matches[HostBindingGroup.Binding] != null) { + // synthetic properties (the ones that have a `@` as a prefix) + // are still treated the same as regular properties. Therefore + // there is no point in storing them in a separate map. properties[matches[HostBindingGroup.Binding]] = 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}; + return {attributes, listeners, properties}; } function compileStyles(styles: string[], selector: string, hostSelector: string): string[] { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index ebe5a834ab..dc3cd9f38b 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -10,7 +10,7 @@ import {flatten, sanitizeIdentifier} from '../../compile_metadata'; import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter'; import {ConstantPool} from '../../constant_pool'; import * as core from '../../core'; -import {AST, ASTWithSource, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEventType, PropertyRead} from '../../expression_parser/ast'; +import {AST, ASTWithSource, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEvent, ParsedEventType, PropertyRead} from '../../expression_parser/ast'; import {Lexer} from '../../expression_parser/lexer'; import {Parser} from '../../expression_parser/parser'; import * as i18n from '../../i18n/i18n_ast'; @@ -29,6 +29,7 @@ import {error} from '../../util'; import * as t from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; import {htmlAstToRender3Ast} from '../r3_template_transform'; +import {getSyntheticPropertyName, prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util'; import {R3QueryMetadata} from './api'; import {I18nContext} from './i18n/context'; @@ -651,11 +652,12 @@ export class TemplateDefinitionBuilder implements t.Visitor