Skip to content

fix(compiler): compile animate.enter and animate.leave listeners to the animation instructions - #597

Merged
Brooooooklyn merged 3 commits into
voidzero-dev:mainfrom
GabeSilvaDev:fix/animate-event-listeners
Oct 22, 2026
Merged

Brooooooklyn merged 3 commits into
voidzero-dev:mainfrom
GabeSilvaDev:fix/animate-event-listeners

Conversation

@GabeSilvaDev

@GabeSilvaDev GabeSilvaDev commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

(animate.enter) and (animate.leave) listeners compile to the wrong instruction, so the handler never runs. For (animate.leave) that also means the element is removed right away instead of when the handler calls animationComplete().

@Component({
  selector: 'app-card',
  template: '<div (animate.leave)="onLeave($event)"></div>',
  host: { '(animate.enter)': 'onEnter($event)', 'animate.leave': 'fade-out' },
})
export class Card {}

OXC on main:

// template
i0.ɵɵlistener("@animate.leave.start", function Card_Template_div_animation_animate_leave_start_0_listener($event) { ... });
// hostBindings
i0.ɵɵlistener("animate.leave", ...); // for '(animate.enter)': a DOM event named "animate.enter"
// host 'animate.leave': 'fade-out' is dropped

ngc, and OXC with this change:

// template
i0.ɵɵanimateLeaveListener(function Card_Template_div_animateleave_0_listener($event) { ... });
// hostBindings
i0.ɵɵanimateEnterListener(function Card_animateenter_HostBindingHandler($event) { ... });
i0.ɵɵanimateLeave("fade-out");

The gaps were in these places:

Piece Change
pipeline/ingest.rs AnimationListenerOp existed in the IR, and naming and reify handled it, but nothing created it. A comment in ingest_listener_owned noted that ParsedEventType::Animation "should be dispatched to AnimationListenerOp by a separate template-path fix". Template and host events of that type now create it, as in ingestElementEvents / ingestHostEvent. The last handler expression becomes a return statement (makeListenerHandlerOps).
directive/compiler.rs, component/transform.rs Host listener names starting with animate. are now ParsedEventType::Animation, as in the binding parser's _parseRegularEvent. They were Regular before. As upstream does, the check runs on the name before any window: / document: / body: target is split off.
reify/statements/misc.rs create_animation_listener_stmt emitted ɵɵsyntheticHostListener("@name.start"/"@name.done", fn). It now emits ɵɵanimateEnterListener(fn) / ɵɵanimateLeaveListener(fn), like animationListener in instruction.ts.
reify/mod.rs Host reify ignored AnimationString ops, so a static animate.enter / animate.leave host attribute was converted and then dropped. It now emits ɵɵanimateEnter / ɵɵanimateLeave, as the template reify already does.

The instructions exist since Angular 20.2.0, the release that added animate.enter / animate.leave. Like the existing ɵɵanimateEnter / ɵɵanimateLeave emit, they are not gated on the Angular version.

Follow-up commit (b007071)

A review against ngc 22.2.2 found three more cases:

Input Before Now (same as ngc)
<ng-template (animate.leave)="..."> ɵɵanimateLeaveListener(...), no consts entry plain ɵɵdomListener("animate.leave", ...) + [3, "animate.leave"], as ingestTemplateBindings does
<div on-animate.enter="..."> ɵɵdomListener("animate.enter", ...) ɵɵanimateEnterListener(...)
(animate.enter.x) handler name ..._animateenterx_0_listener ..._animateenter_x_0_listener (only the first dot is dropped, like name.replace('.', ''))

Tests

  • Six integration tests:
    • an (animate.enter) listener;
    • an (animate.leave) listener with several statements next to a static animate.leave;
    • a host (animate.leave) listener with a static host animate.enter;
    • on-animate.enter;
    • a handler name with an extra dot;
    • (animate.leave) on an <ng-template>.
  • Six e2e compare fixtures in animations/animate-listener.fixture.ts. The four from the first commit fail on main. The two new ones cover on-animate.enter and <ng-template>. All six match ngc 22.2.2; the only diff left is the ɵsetClassDebugInfo path, already listed in known-differences.ts.
  • After merging main:
    • cargo test -p oxc_angular_compiler --all-features: 3484 passed;
    • conformance: 1264/1264;
    • pnpm compare --fixtures: 1161 fixtures, 0 mismatches;
    • pnpm test: 376 passed;
    • pnpm check passes.

One difference is left, and it was already there before this change: when an element has both a static animate.enter and a bound [animate.leave], ngc emits ɵɵanimateLeave before ɵɵanimateEnter and OXC emits them the other way round. Both register on the same element in the creation pass, so behavior is the same. I left it out of this PR.

Not in this PR (already wrong on main; this PR does not touch those paths):

  • a bound host [animate.leave] / @HostBinding('animate.enter') still emits ɵɵdomProperty, where ngc emits ɵɵanimateLeave(fn) in the create block;
  • the partial-declaration linker still puts a static host animate.enter into hostAttrs.

Copilot AI balanced review requested due to automatic review settings October 21, 2026 11:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-22T05:17:26.261723Z b007071 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…he animation instructions

Template listeners compiled to ɵɵsyntheticHostListener("@animate.enter.start", ...), a legacy animation listener, and host listeners to ɵɵlistener("animate.leave", ...), a DOM event that never fires. Create AnimationListenerOp for these events, as Angular's ingest does, and reify it to the enter/leave listener instructions.

A static animate.enter / animate.leave host attribute was dropped because host reify ignored AnimationString ops; emit ɵɵanimateEnter / ɵɵanimateLeave for it.
@GabeSilvaDev
GabeSilvaDev force-pushed the fix/animate-event-listeners branch from 98db00a to b323642 Compare October 21, 2026 11:46
Brooooooklyn and others added 2 commits October 22, 2026 12:50
…efix and dotted names

- `(animate.enter)` / `(animate.leave)` on an explicit `<ng-template>` stays a
  plain listener with a consts entry, as Angular's `ingestTemplateBindings` does.
- `on-animate.enter` is an animation event, like `(animate.enter)`.
- The handler name drops only the first dot of the event name, like
  `name.replace('.', '')`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member

@codex review

@Brooooooklyn
Brooooooklyn merged commit 13a5974 into voidzero-dev:main Oct 22, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants