@@ -31,7 +31,7 @@ use crate::ast::r3::{
3131 SecurityContext ,
3232} ;
3333use crate :: ir:: enums:: {
34- AnimationKind , BindingKind , DeferOpModifierKind , DeferTriggerKind , Namespace , TemplateKind ,
34+ BindingKind , DeferOpModifierKind , DeferTriggerKind , Namespace , TemplateKind ,
3535} ;
3636use crate :: ir:: expression:: {
3737 BinaryExpr , ConditionalCaseExpr , EmptyExpr , IrBinaryOperator , IrExpression , LexicalReadExpr ,
@@ -1775,18 +1775,19 @@ fn ingest_listener_owned<'a>(
17751775 }
17761776 }
17771777
1778- // Determine if this is an animation listener and extract animation phase
1779- let ( is_animation_listener, animation_phase) = match output. event_type {
1778+ // Match Angular's createListenerOp:
1779+ // isLegacyAnimationListener = legacyAnimationPhase !== null
1780+ // The raw phase string is preserved verbatim so that bogus phases (e.g.
1781+ // `@anim.foo`) still emit `ɵɵsyntheticHostListener("@anim.foo", fn)`,
1782+ // matching Angular's reference output. ParsedEventType::Animation has no
1783+ // phase and is currently flagged as an animation listener for parity with
1784+ // the prior behavior; it should be dispatched to AnimationListenerOp by a
1785+ // separate template-path fix.
1786+ let ( is_animation_listener, legacy_animation_phase) = match output. event_type {
17801787 ParsedEventType :: Animation => ( true , None ) ,
17811788 ParsedEventType :: LegacyAnimation => {
1782- // For legacy animations, parse the phase from the output
1783- // Phase can be "start" or "done"
1784- let phase = output. phase . as_ref ( ) . and_then ( |p| match p. as_str ( ) {
1785- "start" => Some ( AnimationKind :: Enter ) ,
1786- "done" => Some ( AnimationKind :: Leave ) ,
1787- _ => None ,
1788- } ) ;
1789- ( true , phase)
1789+ let phase = output. phase . clone ( ) ;
1790+ ( phase. is_some ( ) , phase)
17901791 }
17911792 _ => ( false , None ) ,
17921793 } ;
@@ -1803,7 +1804,7 @@ fn ingest_listener_owned<'a>(
18031804 handler_fn_name : None ,
18041805 consume_fn_name : None ,
18051806 is_animation_listener,
1806- animation_phase ,
1807+ legacy_animation_phase ,
18071808 event_target : output. target ,
18081809 consumes_dollar_event : false , // Set during resolve_dollar_event phase
18091810 } )
@@ -4147,7 +4148,6 @@ fn ingest_host_attribute<'a>(
41474148/// statements to ExpressionStatement ops and the last statement to the return.
41484149fn ingest_host_event < ' a > ( job : & mut HostBindingCompilationJob < ' a > , event : R3BoundEvent < ' a > ) {
41494150 use crate :: ast:: expression:: ParsedEventType ;
4150- use crate :: ir:: enums:: AnimationKind ;
41514151
41524152 let allocator = job. allocator ;
41534153
@@ -4200,22 +4200,24 @@ fn ingest_host_event<'a>(job: &mut HostBindingCompilationJob<'a>, event: R3Bound
42004200 }
42014201 }
42024202
4203- // Determine event target and animation phase based on event type
4204- let ( animation_phase, target) = match event. event_type {
4205- ParsedEventType :: LegacyAnimation => {
4206- // Convert phase string to AnimationKind
4207- let phase = event. phase . as_ref ( ) . and_then ( |p| match p. as_str ( ) {
4208- "start" => Some ( AnimationKind :: Enter ) ,
4209- "done" => Some ( AnimationKind :: Leave ) ,
4210- _ => None ,
4211- } ) ;
4212- ( phase, None )
4213- }
4203+ // Match Angular's createListenerOp (compiler/src/template/pipeline/ir/src/ops/create.ts):
4204+ // isLegacyAnimationListener = legacyAnimationPhase !== null
4205+ //
4206+ // The raw phase string is preserved verbatim. Angular's binding parser already
4207+ // lowercased + trimmed it via `splitAtPeriod` + `.toLowerCase()`, so `@anim.START`
4208+ // arrives here as `start`. Phases other than `start`/`done` (e.g. `@anim.foo`) are
4209+ // not silently dropped — they round-trip into the emitted instruction so the
4210+ // output matches Angular byte-for-byte.
4211+ //
4212+ // For host events the binding parser only produces Regular or LegacyAnimation
4213+ // (Animation is template-only), and a LegacyAnimation event with no phase (e.g.
4214+ // `@HostListener('@anim')`) leaves is_animation_listener=false so reify emits a
4215+ // plain ɵɵlistener — matching Angular's `isLegacyAnimationListener=false` path.
4216+ let ( legacy_animation_phase, target) = match event. event_type {
4217+ ParsedEventType :: LegacyAnimation => ( event. phase . clone ( ) , None ) ,
42144218 _ => ( None , event. target . clone ( ) ) ,
42154219 } ;
4216-
4217- // Check if this is an animation event
4218- let is_animation = matches ! ( event. event_type, ParsedEventType :: Animation ) ;
4220+ let is_animation = legacy_animation_phase. is_some ( ) ;
42194221
42204222 let op = CreateOp :: Listener ( ListenerOp {
42214223 base : CreateOpBase { source_span : Some ( event. source_span ) , ..Default :: default ( ) } ,
@@ -4229,7 +4231,7 @@ fn ingest_host_event<'a>(job: &mut HostBindingCompilationJob<'a>, event: R3Bound
42294231 handler_fn_name : None ,
42304232 consume_fn_name : None ,
42314233 is_animation_listener : is_animation,
4232- animation_phase ,
4234+ legacy_animation_phase ,
42334235 event_target : target,
42344236 consumes_dollar_event : false , // Set during resolve_dollar_event phase
42354237 } ) ;
0 commit comments