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
39 changes: 28 additions & 11 deletions crates/oxc_angular_compiler/src/pipeline/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3132,6 +3132,7 @@ fn ingest_defer_view<'a>(
job: &mut ComponentCompilationJob<'a>,
parent_xref: XrefId,
suffix: &str,
i18n: Option<I18nMeta<'a>>,
children: Option<Vec<'a, R3Node<'a>>>,
source_span: Option<oxc_span::Span>,
) -> Option<XrefId> {
Expand All @@ -3151,6 +3152,11 @@ fn ingest_defer_view<'a>(
// We use the same pattern here so that defer_resolve_targets can find elements by view xref.
let fn_name_suffix = Some(Atom::from(job.allocator.alloc_str(&format!("Defer{suffix}"))));

// Convert i18n metadata to placeholder, matching Angular's ingestDeferView which passes
// i18nMeta through to createTemplateOp. This enables propagate_i18n_blocks to wrap the
// deferred template with i18nStart/i18nEnd when inside an i18n context.
let i18n_placeholder = convert_i18n_meta_to_placeholder(i18n);

let template_op = CreateOp::Template(TemplateOp {
base: CreateOpBase { source_span, ..Default::default() },
xref: secondary_view, // Use view xref as TemplateOp xref, matching Angular
Expand All @@ -3166,7 +3172,7 @@ fn ingest_defer_view<'a>(
attributes: None,
local_refs: Vec::new_in(job.allocator),
local_refs_index: None,
i18n_placeholder: None,
i18n_placeholder,
});

// Push the TemplateOp to the parent view's create ops
Expand All @@ -3185,7 +3191,7 @@ fn ingest_defer_block<'a>(
) {
let xref = job.allocate_xref_id();

// Extract timing values and source spans before consuming the blocks
// Extract timing values, source spans, and i18n metadata before consuming the blocks
let placeholder_minimum_time = defer_block.placeholder.as_ref().and_then(|p| p.minimum_time);
let loading_minimum_time = defer_block.loading.as_ref().and_then(|l| l.minimum_time);
let loading_after_time = defer_block.loading.as_ref().and_then(|l| l.after_time);
Expand All @@ -3199,33 +3205,44 @@ fn ingest_defer_block<'a>(
job,
view_xref,
"", // Empty suffix for main content - becomes "Defer"
defer_block.i18n,
Some(defer_block.children),
Some(defer_block.source_span),
);

// Destructure sub-blocks to extract both children and i18n before consuming
let (loading_children, loading_i18n) = match defer_block.loading {
Some(l) => (Some(l.children), l.i18n),
None => (None, None),
};
let loading_template_xref = ingest_defer_view(
job,
view_xref,
"Loading",
defer_block.loading.map(|l| l.children),
loading_i18n,
loading_children,
loading_source_span,
);

let (placeholder_children, placeholder_i18n) = match defer_block.placeholder {
Some(p) => (Some(p.children), p.i18n),
None => (None, None),
};
let placeholder_template_xref = ingest_defer_view(
job,
view_xref,
"Placeholder",
defer_block.placeholder.map(|p| p.children),
placeholder_i18n,
placeholder_children,
placeholder_source_span,
);

let error_template_xref = ingest_defer_view(
job,
view_xref,
"Error",
defer_block.error.map(|e| e.children),
error_source_span,
);
let (error_children, error_i18n) = match defer_block.error {
Some(e) => (Some(e.children), e.i18n),
None => (None, None),
};
let error_template_xref =
ingest_defer_view(job, view_xref, "Error", error_i18n, error_children, error_source_span);

// Set own_resolver_fn based on emit mode
// This matches Angular's ingestDeferBlock behavior (ingest.ts lines 663-672)
Expand Down
44 changes: 40 additions & 4 deletions crates/oxc_angular_compiler/src/transform/html_to_r3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2461,14 +2461,23 @@ impl<'a> HtmlToR3Transform<'a> {
connected.parameters.iter().map(|p| p.expression.as_str()).collect();
let minimum_time = parse_placeholder_parameters(&params);

// Create i18n placeholder if inside an i18n context
let i18n = self.create_block_placeholder(
"placeholder",
&[],
connected.span,
connected.start_span,
connected.end_span,
);

placeholder = Some(R3DeferredBlockPlaceholder {
children: connected_children,
minimum_time,
source_span: connected.span,
name_span: connected.name_span,
start_source_span: connected.start_span,
end_source_span: connected.end_span,
i18n: None,
i18n,
});
}
BlockType::Loading => {
Expand All @@ -2477,6 +2486,15 @@ impl<'a> HtmlToR3Transform<'a> {
connected.parameters.iter().map(|p| p.expression.as_str()).collect();
let (after_time, minimum_time) = parse_loading_parameters(&params);

// Create i18n placeholder if inside an i18n context
let i18n = self.create_block_placeholder(
"loading",
&[],
connected.span,
connected.start_span,
connected.end_span,
);

loading = Some(R3DeferredBlockLoading {
children: connected_children,
after_time,
Expand All @@ -2485,17 +2503,26 @@ impl<'a> HtmlToR3Transform<'a> {
name_span: connected.name_span,
start_source_span: connected.start_span,
end_source_span: connected.end_span,
i18n: None,
i18n,
});
}
BlockType::Error => {
// Create i18n placeholder if inside an i18n context
let i18n = self.create_block_placeholder(
"error",
&[],
connected.span,
connected.start_span,
connected.end_span,
);

error = Some(R3DeferredBlockError {
children: connected_children,
source_span: connected.span,
name_span: connected.name_span,
start_source_span: connected.start_span,
end_source_span: connected.end_span,
i18n: None,
i18n,
});
}
_ => {}
Expand All @@ -2512,6 +2539,15 @@ impl<'a> HtmlToR3Transform<'a> {
block.span
};

// Create i18n placeholder for @defer block if inside i18n context
let i18n = self.create_block_placeholder(
"defer",
&[],
source_span,
block.start_span,
end_source_span,
);

let defer_block = R3DeferredBlock {
children,
triggers: trigger_result.triggers,
Expand All @@ -2525,7 +2561,7 @@ impl<'a> HtmlToR3Transform<'a> {
name_span: block.name_span,
start_source_span: block.start_span,
end_source_span,
i18n: None,
i18n,
};
Some(R3Node::DeferredBlock(Box::new_in(defer_block, self.allocator)))
}
Expand Down
81 changes: 81 additions & 0 deletions crates/oxc_angular_compiler/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,87 @@ fn test_defer_block() {
insta::assert_snapshot!("defer_block", js);
}

/// Tests that @defer blocks inside i18n contexts get wrapped with i18nStart/i18nEnd.
/// Angular propagates i18n context into defer view templates so that the deferred
/// content is part of the i18n message. Each defer sub-block (main, loading,
/// placeholder, error) gets its own sub-template index.
///
/// Ported from Angular compliance test:
/// `r3_view_compiler_i18n/blocks/defer.ts`
#[test]
fn test_defer_inside_i18n() {
let js = compile_template_to_js(
r#"<div i18n>
Content:
@defer (when isLoaded) {
before<span>middle</span>after
} @placeholder {
before<div>placeholder</div>after
} @loading {
before<button>loading</button>after
} @error {
before<h1>error</h1>after
}
</div>"#,
"MyApp",
);

// Each deferred template function should be wrapped with i18nStart/i18nEnd
// with increasing sub-template indices (1, 2, 3, 4)
assert!(
js.contains("i18nStart(0,0,1)"),
"Main defer template should have i18nStart with sub-template index 1. Output:\n{js}"
);
assert!(
js.contains("i18nStart(0,0,2)"),
"Loading defer template should have i18nStart with sub-template index 2. Output:\n{js}"
);
assert!(
js.contains("i18nStart(0,0,3)"),
"Placeholder defer template should have i18nStart with sub-template index 3. Output:\n{js}"
);
assert!(
js.contains("i18nStart(0,0,4)"),
"Error defer template should have i18nStart with sub-template index 4. Output:\n{js}"
);

// The deferred templates should have 2 decls (i18nStart + element), not 1
// domTemplate(N, fn, 2, 0) - 2 declarations for each deferred view
assert!(
js.contains("MyApp_Defer_2_Template,2,0)"),
"Main defer domTemplate should have 2 decls. Output:\n{js}"
);

insta::assert_snapshot!("defer_inside_i18n", js);
}

/// When @defer is nested inside a structural directive (*ngIf template) that's inside
/// an i18n context, the i18n wrapping must propagate through the template boundary
/// to the defer view. This matches the unlock-view-confirm ClickUp pattern.
#[test]
fn test_defer_inside_structural_directive_in_i18n() {
let js = compile_template_to_js(
r#"<div i18n>
text
<span *ngIf="show">
@defer (on idle) {
<span>deferred</span>
}
</span>
</div>"#,
"MyApp",
);

// The defer template should have i18nStart wrapping since it's
// transitively inside an i18n context (through the *ngIf template)
assert!(
js.contains("i18nStart(0,"),
"Defer template inside structural directive in i18n should have i18nStart. Output:\n{js}"
);

insta::assert_snapshot!("defer_inside_structural_directive_in_i18n", js);
}

#[test]
fn test_defer_with_loading() {
let js = compile_template_to_js(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
expression: js
---
function MyApp_Defer_2_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵi18nStart(0,0,1);
i0.ɵɵelement(1,"span");
i0.ɵɵi18nEnd();
}
}
function MyApp_DeferLoading_3_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵi18nStart(0,0,2);
i0.ɵɵelement(1,"button");
i0.ɵɵi18nEnd();
}
}
function MyApp_DeferPlaceholder_4_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵi18nStart(0,0,3);
i0.ɵɵelement(1,"div");
i0.ɵɵi18nEnd();
}
}
function MyApp_DeferError_5_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵi18nStart(0,0,4);
i0.ɵɵelement(1,"h1");
i0.ɵɵi18nEnd();
}
}
function MyApp_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵelementStart(0,"div");
i0.ɵɵi18nStart(1,0);
i0.ɵɵdomTemplate(2,MyApp_Defer_2_Template,2,0)(3,MyApp_DeferLoading_3_Template,2,
0)(4,MyApp_DeferPlaceholder_4_Template,2,0)(5,MyApp_DeferError_5_Template,2,
0);
i0.ɵɵdefer(6,2,null,3,4,5);
i0.ɵɵi18nEnd();
i0.ɵɵelementEnd();
}
if ((rf & 2)) {
i0.ɵɵadvance(6);
i0.ɵɵdeferWhen(ctx.isLoaded);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
expression: js
---
function MyApp_span_2_Defer_2_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵi18nStart(0,0,2);
i0.ɵɵelement(1,"span");
i0.ɵɵi18nEnd();
}
}
function MyApp_span_2_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵi18nStart(0,0,1);
i0.ɵɵelementStart(1,"span");
i0.ɵɵdomTemplate(2,MyApp_span_2_Defer_2_Template,2,0);
i0.ɵɵdefer(3,2);
i0.ɵɵdeferOnIdle();
i0.ɵɵelementEnd();
i0.ɵɵi18nEnd();
}
}
function MyApp_Template(rf,ctx) {
if ((rf & 1)) {
i0.ɵɵelementStart(0,"div");
i0.ɵɵi18nStart(1,0);
i0.ɵɵtemplate(2,MyApp_span_2_Template,5,0,"span",1);
i0.ɵɵi18nEnd();
i0.ɵɵelementEnd();
}
if ((rf & 2)) {
i0.ɵɵadvance(2);
i0.ɵɵproperty("ngIf",ctx.show);
}
}
Loading