Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,11 @@ fn optimize_listener_handler_ops<'a>(job: &mut ComponentCompilationJob<'a>) {
}
CreateOp::Animation(animation) => {
optimize_handler_ops(&mut animation.handler_ops, None, allocator);
optimize_save_restore_view(&mut animation.handler_ops, allocator);
// Note: We intentionally do NOT call optimize_save_restore_view on
// Animation handler_ops. Angular's ngtsc output keeps restoreView/resetView
// in animation callbacks even when the return value doesn't reference the
// view context (e.g., `return "animate-in"`). Skipping this optimization
// for Animation handlers matches the observed Angular output.
}
_ => {}
}
Expand Down
84 changes: 84 additions & 0 deletions crates/oxc_angular_compiler/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3325,6 +3325,90 @@ fn test_animation_in_for_with_listener_variable_naming() {
insta::assert_snapshot!("animation_in_for_with_listener", js);
}

#[test]
fn test_animation_enter_string_literal_in_embedded_view() {
// Reproduces the edit-long-text-custom-field-value.component.ts issue:
// An animation handler that returns a string literal in an embedded view (@if)
// should still have getCurrentView/restoreView/resetView generated.
//
// Angular always adds restoreView/resetView to animation handlers in embedded views,
// even when the handler expression is a simple string literal.
// The variable_optimization phase may later optimize away the restoreView if the
// handler doesn't actually access outer context, but the getCurrentView at the
// view level must survive if there are other listeners that reference it.
//
// NG output pattern for animation returning string literal in embedded view:
// const _r1 = i0.ɵɵgetCurrentView();
// i0.ɵɵelementStart(0, "div", 0);
// i0.ɵɵanimateEnter(function ..._cb() {
// i0.ɵɵrestoreView(_r1);
// const ctx_r1 = i0.ɵɵnextContext();
// return i0.ɵɵresetView(ctx_r1.onClick());
// });
// i0.ɵɵlistener("click", function ..._listener() { ... });
//
// OXC bug: missing getCurrentView because the animation handler's restoreView
// gets optimized away (string literal doesn't reference outer context), and if
// the SavedView variable optimization also removes the getCurrentView, other
// listeners in the same view lose their restoreView target.
let js = compile_template_to_js(
r#"@if (show) {
<div [animate.enter]="'animate-in'" (click)="onClick()">
{{label}}
</div>
}"#,
"TestComponent",
);
assert!(
!js.contains("_unnamed_"),
"Generated JS contains _unnamed_ references.\nGenerated JS:\n{js}"
);
assert!(
js.contains("getCurrentView"),
"Embedded view with animation and listener should have getCurrentView.\nGenerated JS:\n{js}"
);
assert!(
js.contains("restoreView"),
"Listener in embedded view should have restoreView.\nGenerated JS:\n{js}"
);
insta::assert_snapshot!("animation_enter_string_literal_embedded_view", js);
}

#[test]
fn test_animation_enter_string_literal_only_in_embedded_view() {
// Tests the case where an animation handler returning a string literal is the ONLY
// listener-like op in an embedded view. Angular's ngtsc always keeps restoreView/resetView
// in animation handler callbacks in embedded views, even when the return value is a simple
// string literal that doesn't reference the view context.
//
// Expected NG output pattern:
// i0.ɵɵanimateEnter(function ...() {
// i0.ɵɵrestoreView(_r1);
// return i0.ɵɵresetView("animate-in");
// });
let js = compile_template_to_js(
r#"@if (show) {
<div [animate.enter]="'animate-in'">
{{label}}
</div>
}"#,
"TestComponent",
);
assert!(
!js.contains("_unnamed_"),
"Generated JS contains _unnamed_ references.\nGenerated JS:\n{js}"
);
assert!(
js.contains("restoreView"),
"Animation handler in embedded view should keep restoreView.\nGenerated JS:\n{js}"
);
assert!(
js.contains("resetView"),
"Animation handler in embedded view should keep resetView.\nGenerated JS:\n{js}"
);
insta::assert_snapshot!("animation_enter_string_literal_only_embedded_view", js);
}

/// Test that implicit standalone components (no `standalone` in decorator) use Full mode.
///
/// Angular 19+ defaults `standalone` to `true` when not specified. However, OXC performs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
expression: js
---
function TestComponent_Conditional_0_Template(rf,ctx) {
if ((rf & 1)) {
const _r1 = i0.ɵɵgetCurrentView();
i0.ɵɵtext(0,"\n ");
i0.ɵɵelementStart(1,"div",0);
i0.ɵɵanimateEnter(function TestComponent_Conditional_0_Template_animateenter_cb() {
i0.ɵɵrestoreView(_r1);
return i0.ɵɵresetView("animate-in");
});
i0.ɵɵlistener("click",function TestComponent_Conditional_0_Template_div_click_1_listener() {
i0.ɵɵrestoreView(_r1);
const ctx_r1 = i0.ɵɵnextContext();
return i0.ɵɵresetView(ctx_r1.onClick());
});
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
i0.ɵɵtext(3,"\n ");
}
if ((rf & 2)) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1("\n ",ctx_r1.label,"\n ");
}
}
function TestComponent_Template(rf,ctx) {
if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,4,1); }
if ((rf & 2)) { i0.ɵɵconditional((ctx.show? 0: -1)); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
expression: js
---
function TestComponent_Conditional_0_Template(rf,ctx) {
if ((rf & 1)) {
const _r1 = i0.ɵɵgetCurrentView();
i0.ɵɵtext(0,"\n ");
i0.ɵɵelementStart(1,"div");
i0.ɵɵanimateEnter(function TestComponent_Conditional_0_Template_animateenter_cb() {
i0.ɵɵrestoreView(_r1);
return i0.ɵɵresetView("animate-in");
});
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
i0.ɵɵtext(3,"\n ");
}
if ((rf & 2)) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1("\n ",ctx_r1.label,"\n ");
}
}
function TestComponent_Template(rf,ctx) {
if ((rf & 1)) { i0.ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,4,1); }
if ((rf & 2)) { i0.ɵɵconditional((ctx.show? 0: -1)); }
}
Loading