diff --git a/crates/oxc_angular_compiler/src/transform/html_to_r3.rs b/crates/oxc_angular_compiler/src/transform/html_to_r3.rs index e4796d8dd..1e80f1b46 100644 --- a/crates/oxc_angular_compiler/src/transform/html_to_r3.rs +++ b/crates/oxc_angular_compiler/src/transform/html_to_r3.rs @@ -468,18 +468,27 @@ impl<'a> HtmlToR3Transform<'a> { let selector = self.get_ng_content_selector(element); self.ng_content_selectors.push(selector.clone()); - // For ng-content, include the structural directive attribute (*ngIf, etc.) - // as a text attribute. This is needed because the projection instruction - // includes these attributes in its output (e.g., ["*ngIf", "!subtitle()"]). - // Reference: r3_template_transform.ts line 193 - all attrs are included - let mut content_attributes = attributes; - if let Some(ref tpl_attr) = template_attr { + // For ng-content, Angular converts ALL raw HTML attributes to TextAttributes. + // Reference: r3_template_transform.ts line 193: + // const attrs: t.TextAttribute[] = element.attrs.map((attr) => this.visitAttribute(attr)); + // This includes bound attributes like [select]="..." which get serialized with + // their raw names (e.g., "[select]") and values into the projection instruction. + // + // However, i18n/i18n-* attributes are excluded because Angular's I18nMetaVisitor + // strips them from element.attrs before r3_template_transform runs. + let mut content_attributes: Vec<'a, R3TextAttribute<'a>> = + Vec::with_capacity_in(element.attrs.len(), self.allocator); + for attr in &element.attrs { + let name = attr.name.as_str(); + if name == "i18n" || name.starts_with("i18n-") { + continue; + } content_attributes.push(R3TextAttribute { - name: tpl_attr.name.clone(), - value: tpl_attr.value.clone(), - source_span: tpl_attr.span, - key_span: Some(tpl_attr.name_span), - value_span: tpl_attr.value_span, + name: attr.name.clone(), + value: attr.value.clone(), + source_span: attr.span, + key_span: Some(attr.name_span), + value_span: attr.value_span, i18n: None, }); } diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 880a42e57..b14e4da31 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -756,6 +756,36 @@ fn test_ng_content_select() { insta::assert_snapshot!("ng_content_select", js); } +#[test] +fn test_ng_content_i18n_attr_not_in_projection() { + // Verify i18n/i18n-* attrs are NOT included in ng-content projection attributes. + // Angular's I18nMetaVisitor strips these before r3_template_transform runs. + let js = compile_template_to_js( + r#""#, + "TestComponent", + ); + assert!( + !js.contains(r#""i18n""#), + "i18n attribute should not appear in projection output. Got:\n{js}" + ); +} + +#[test] +fn test_ng_content_with_bound_select() { + // Tests that [select] binding on ng-content passes the binding name and value + // as attributes to the projection instruction. + // Angular treats ALL raw attrs on ng-content as TextAttributes, including bindings. + // [select] with brackets is NOT the same as the static `select` attribute for the + // CSS selector — the selector stays as "*" (wildcard). + // Expected: ɵɵprojectionDef() with no args (single wildcard), + // ɵɵprojection(0, 0, ["[select]", "'[slot=expanded-content]'"]) + let js = compile_template_to_js( + r#""#, + "TestComponent", + ); + insta::assert_snapshot!("ng_content_with_bound_select", js); +} + #[test] fn test_ng_content_with_ng_project_as() { // Tests that ngProjectAs attribute generates the correct ProjectAs marker (5) diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__ng_content_with_bound_select.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__ng_content_with_bound_select.snap new file mode 100644 index 000000000..ddaa731f2 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__ng_content_with_bound_select.snap @@ -0,0 +1,11 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +expression: js +--- +const _c0 = ["*"]; +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵprojectionDef(); + i0.ɵɵprojection(0,0,["[select]","'[slot=expanded-content]'"]); + } +}