diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs
index 04056b5d4..61aa6d88b 100644
--- a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs
+++ b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs
@@ -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.
}
_ => {}
}
diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs
index 690b98a35..556d2d915 100644
--- a/crates/oxc_angular_compiler/tests/integration_test.rs
+++ b/crates/oxc_angular_compiler/tests/integration_test.rs
@@ -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) {
+
+ {{label}}
+
+ }"#,
+ "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) {
+
+ {{label}}
+
+ }"#,
+ "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
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__animation_enter_string_literal_embedded_view.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__animation_enter_string_literal_embedded_view.snap
new file mode 100644
index 000000000..1aa749796
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__animation_enter_string_literal_embedded_view.snap
@@ -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)); }
+}
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__animation_enter_string_literal_only_embedded_view.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__animation_enter_string_literal_only_embedded_view.snap
new file mode 100644
index 000000000..eb80542a9
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__animation_enter_string_literal_only_embedded_view.snap
@@ -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)); }
+}