Skip to content

Commit d8a82ef

Browse files
Brooooooklynclaude
andcommitted
fix: ng-content with bound attributes like [select] now passes attrs to projection instruction
Angular's r3_template_transform.ts converts ALL raw HTML attributes on ng-content to TextAttributes, including binding syntax like [select]="...". OXC was only using categorized text attributes, dropping bound attributes entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aabfc01 commit d8a82ef

3 files changed

Lines changed: 40 additions & 11 deletions

File tree

crates/oxc_angular_compiler/src/transform/html_to_r3.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -468,18 +468,20 @@ impl<'a> HtmlToR3Transform<'a> {
468468
let selector = self.get_ng_content_selector(element);
469469
self.ng_content_selectors.push(selector.clone());
470470

471-
// For ng-content, include the structural directive attribute (*ngIf, etc.)
472-
// as a text attribute. This is needed because the projection instruction
473-
// includes these attributes in its output (e.g., ["*ngIf", "!subtitle()"]).
474-
// Reference: r3_template_transform.ts line 193 - all attrs are included
475-
let mut content_attributes = attributes;
476-
if let Some(ref tpl_attr) = template_attr {
471+
// For ng-content, Angular converts ALL raw HTML attributes to TextAttributes.
472+
// Reference: r3_template_transform.ts line 193:
473+
// const attrs: t.TextAttribute[] = element.attrs.map((attr) => this.visitAttribute(attr));
474+
// This includes bound attributes like [select]="..." which get serialized with
475+
// their raw names (e.g., "[select]") and values into the projection instruction.
476+
let mut content_attributes: Vec<'a, R3TextAttribute<'a>> =
477+
Vec::with_capacity_in(element.attrs.len(), self.allocator);
478+
for attr in &element.attrs {
477479
content_attributes.push(R3TextAttribute {
478-
name: tpl_attr.name.clone(),
479-
value: tpl_attr.value.clone(),
480-
source_span: tpl_attr.span,
481-
key_span: Some(tpl_attr.name_span),
482-
value_span: tpl_attr.value_span,
480+
name: attr.name.clone(),
481+
value: attr.value.clone(),
482+
source_span: attr.span,
483+
key_span: Some(attr.name_span),
484+
value_span: attr.value_span,
483485
i18n: None,
484486
});
485487
}

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,22 @@ fn test_ng_content_select() {
756756
insta::assert_snapshot!("ng_content_select", js);
757757
}
758758

759+
#[test]
760+
fn test_ng_content_with_bound_select() {
761+
// Tests that [select] binding on ng-content passes the binding name and value
762+
// as attributes to the projection instruction.
763+
// Angular treats ALL raw attrs on ng-content as TextAttributes, including bindings.
764+
// [select] with brackets is NOT the same as the static `select` attribute for the
765+
// CSS selector — the selector stays as "*" (wildcard).
766+
// Expected: ɵɵprojectionDef() with no args (single wildcard),
767+
// ɵɵprojection(0, 0, ["[select]", "'[slot=expanded-content]'"])
768+
let js = compile_template_to_js(
769+
r#"<ng-content [select]="'[slot=expanded-content]'" />"#,
770+
"TestComponent",
771+
);
772+
insta::assert_snapshot!("ng_content_with_bound_select", js);
773+
}
774+
759775
#[test]
760776
fn test_ng_content_with_ng_project_as() {
761777
// Tests that ngProjectAs attribute generates the correct ProjectAs marker (5)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: js
4+
---
5+
const _c0 = ["*"];
6+
function TestComponent_Template(rf,ctx) {
7+
if ((rf & 1)) {
8+
i0.ɵɵprojectionDef();
9+
i0.ɵɵprojection(0,0,["[select]","'[slot=expanded-content]'"]);
10+
}
11+
}

0 commit comments

Comments
 (0)