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, LocalResolver const value = input.value.visit(this._valueConverter); // setProperty without a value doesn't make any sense if (value.name || value.value) { + const bindingName = prepareSyntheticPropertyName(input.name); this.allocateBindingSlots(value); - const name = prepareSyntheticAttributeName(input.name); this.updateInstruction(input.sourceSpan, R3.elementProperty, () => { return [ - o.literal(elementIndex), o.literal(name), this.convertPropertyBinding(implicit, value) + o.literal(elementIndex), o.literal(bindingName), + this.convertPropertyBinding(implicit, value) ]; }); } @@ -1002,7 +1004,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (isASTWithSource(valueExp)) { const literal = valueExp.ast; if (isLiteralPrimitive(literal) && literal.value === undefined) { - addAttrExpr(prepareSyntheticAttributeName(input.name), EMPTY_STRING_EXPR); + addAttrExpr(prepareSyntheticPropertyName(input.name), EMPTY_STRING_EXPR); } } } else { @@ -1021,7 +1023,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (nonSyntheticInputs.length || outputs.length) { addAttrExpr(core.AttributeMarker.SelectOnly); nonSyntheticInputs.forEach((i: t.BoundAttribute) => addAttrExpr(i.name)); - outputs.forEach((o: t.BoundEvent) => addAttrExpr(o.name)); + outputs.forEach((o: t.BoundEvent) => { + const name = + o.type === ParsedEventType.Animation ? getSyntheticPropertyName(o.name) : o.name; + addAttrExpr(name); + }); } return attrExprs; @@ -1064,14 +1070,19 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private prepareListenerParameter(tagName: string, outputAst: t.BoundEvent, index: number): () => o.Expression[] { let eventName: string = outputAst.name; + + let bindingFnName; if (outputAst.type === ParsedEventType.Animation) { - eventName = prepareSyntheticAttributeName(`${outputAst.name}.${outputAst.phase}`); + // synthetic @listener.foo values are treated the exact same as are standard listeners + bindingFnName = prepareSyntheticListenerFunctionName(eventName, outputAst.phase !); + eventName = prepareSyntheticListenerName(eventName, outputAst.phase !); + } else { + bindingFnName = sanitizeIdentifier(eventName); } - const evNameSanitized = sanitizeIdentifier(eventName); + const tagNameSanitized = sanitizeIdentifier(tagName); const functionName = - `${this.templateName}_${tagNameSanitized}_${evNameSanitized}_${index}_listener`; - + `${this.templateName}_${tagNameSanitized}_${bindingFnName}_${index}_listener`; return () => { const listenerScope = this._bindingScope.nestedScope(this._bindingScope.bindingLevel); @@ -1563,10 +1574,6 @@ function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityCo } } -function prepareSyntheticAttributeName(name: string) { - return '@' + name; -} - function isSingleElementTemplate(children: t.Node[]): children is[t.Element] { return children.length === 1 && children[0] instanceof t.Element; } diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 9c137277dd..178fa7c58c 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -440,4 +440,4 @@ export function calcPossibleSecurityContexts( elementName => registry.securityContext(elementName, propName, isAttribute))); }); return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort(); -} +} \ No newline at end of file diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index f384eba657..1275d5fe55 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -83,6 +83,7 @@ export { loadQueryList as ɵloadQueryList, elementEnd as ɵelementEnd, elementProperty as ɵelementProperty, + componentHostSyntheticProperty as ɵcomponentHostSyntheticProperty, projectionDef as ɵprojectionDef, reference as ɵreference, enableBindings as ɵenableBindings, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index f1971992b5..6e5ba0a271 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -44,6 +44,7 @@ export { elementClassProp, elementEnd, elementProperty, + componentHostSyntheticProperty, elementStart, elementContainerStart, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 03ffe747ec..97e67366bf 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -987,10 +987,48 @@ export function elementAttribute( * @param nativeOnly Whether or not we should only set native properties and skip input check * (this is necessary for host property bindings) */ - export function elementProperty( index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): void { + elementPropertyInternal(index, propName, value, sanitizer, nativeOnly); +} + +/** + * Updates a synthetic host binding (e.g. `[@foo]`) on a component. + * + * This instruction is for compatibility purposes and is designed to ensure that a + * synthetic host binding (e.g. `@HostBinding('@foo')`) properly gets rendered in + * the component's renderer. Normally all host bindings are evaluated with the parent + * component's renderer, but, in the case of animation @triggers, they need to be + * evaluated with the sub components renderer (because that's where the animation + * triggers are defined). + * + * Do not use this instruction as a replacement for `elementProperty`. This instruction + * only exists to ensure compatibility with the ViewEngine's host binding behavior. + * + * @param index The index of the element to update in the data array + * @param propName Name of property. Because it is going to DOM, this is not subject to + * renaming as part of minification. + * @param value New value to write. + * @param sanitizer An optional function used to sanitize the value. + * @param nativeOnly Whether or not we should only set native properties and skip input check + * (this is necessary for host property bindings) + */ +export function componentHostSyntheticProperty( + index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, + nativeOnly?: boolean) { + elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer); +} + +function loadComponentRenderer(tNode: TNode, lView: LView): Renderer3 { + const componentLView = lView[tNode.index] as LView; + return componentLView[RENDERER]; +} + +function elementPropertyInternal( + index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, + nativeOnly?: boolean, + loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void { if (value === NO_CHANGE) return; const lView = getLView(); const element = getNativeByIndex(index, lView) as RElement | RComment; @@ -1007,7 +1045,7 @@ export function elementProperty( } } } else if (tNode.type === TNodeType.Element) { - const renderer = lView[RENDERER]; + const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER]; // It is assumed that the sanitizer is only added when the compiler determines that the property // is risky, so sanitization can be done without further checks. value = sanitizer != null ? (sanitizer(value) as any) : value; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 1a30e41a4c..5ce7a8fb08 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -78,6 +78,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵload': r3.load, 'ɵprojection': r3.projection, 'ɵelementProperty': r3.elementProperty, + 'ɵcomponentHostSyntheticProperty': r3.componentHostSyntheticProperty, 'ɵpipeBind1': r3.pipeBind1, 'ɵpipeBind2': r3.pipeBind2, 'ɵpipeBind3': r3.pipeBind3, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 7cef770f1f..86140e2e12 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -563,6 +563,9 @@ { "name": "elementProperty" }, + { + "name": "elementPropertyInternal" + }, { "name": "elementStart" }, diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index b6fbe03438..0a4c784e41 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -2102,6 +2102,49 @@ describe('render3 integration test', () => { const [elm, attr, value] = spy.calls.mostRecent().args; expect(attr).toEqual('@fooAnimation'); }); + + it('should allow host binding animations to be picked up and rendered', () => { + class ChildCompWithAnim { + static ngDirectiveDef = defineDirective({ + type: ChildCompWithAnim, + factory: () => new ChildCompWithAnim(), + selectors: [['child-comp-with-anim']], + hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void { + if (rf & RenderFlags.Update) { + elementProperty(0, '@fooAnim', ctx.exp); + } + }, + }); + + exp = 'go'; + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + consts: 1, + vars: 1, + selectors: [['foo']], + factory: () => new ParentComp(), + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + element(0, 'child-comp-with-anim'); + } + }, + directives: [ChildCompWithAnim] + }); + } + + const rendererFactory = new MockRendererFactory(['setProperty']); + const fixture = new ComponentFixture(ParentComp, {rendererFactory}); + + const renderer = rendererFactory.lastRenderer !; + fixture.update(); + + const spy = renderer.spies['setProperty']; + const [elm, attr, value] = spy.calls.mostRecent().args; + expect(attr).toEqual('@fooAnim'); + }); }); describe('element discovery', () => {