From eb6ef5a936fdcdc20d127f13fd01f4cf8d454658 Mon Sep 17 00:00:00 2001 From: 81reap Date: Thu, 29 Oct 2026 18:29:40 -0400 Subject: [PATCH] fix(templates) :: let `each_row` call inline partials Fixes #846 Assisted By :: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ src/render.rs | 19 +++++++++++++++++++ src/templates.rs | 23 +++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb9eb9d2..c3d02090f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Each item of the tracking component accepts a `link`. ([#900](https://github.com/sqlpage/SQLPage/issues/900)) +- Custom components can call inline partials inside `each_row` and after it, when the partials are defined before `each_row`. ([#846](https://github.com/sqlpage/SQLPage/issues/846)) + ## v0.47.0 (2026-10-15) - Redesigned the official website homepage with live SQLPage examples, an interactive database sculpture, flowing section transitions, responsive layouts, and direct links to documentation and deployment guides. - For sites that store their pages in a database, checking for page updates no longer makes other pages wait when the database is busy. In a test, another page loaded in under 1 ms instead of about 250 ms. diff --git a/src/render.rs b/src/render.rs index 54652b58e..ebbe9ab8d 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1187,6 +1187,25 @@ mod tests { Ok(()) } + #[actix_web::test] + async fn inline_partials_defined_before_each_row_render_in_every_part() -> anyhow::Result<()> { + let template = Template::compile( + "{{#*inline \"item\"}}[{{x}}]{{/inline}}\ + {{> item x=\"start\"}}{{#each_row}}{{> item}}{{/each_row}}{{> item x=\"end\"}}", + )?; + let split = split_template(template); + let mut output = Vec::new(); + let config = app_config::tests::test_config(); + let app_state = Arc::new(AppState::init(&config).await.unwrap()); + let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state, 0, 0); + rdr.render_start(&mut output, json!(null))?; + rdr.render_item(&mut output, json!({"x": 1}))?; + rdr.render_item(&mut output, json!({"x": 2}))?; + rdr.render_end(&mut output)?; + assert_eq!(String::from_utf8_lossy(&output), "[start][1][2][end]"); + Ok(()) + } + #[actix_web::test] async fn test_delayed() -> anyhow::Result<()> { let template = Template::compile( diff --git a/src/templates.rs b/src/templates.rs index 3c36506fb..16ceaca1c 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -44,6 +44,10 @@ pub fn split_template(mut original: Template) -> SplitTemplate { after_list.name = Some(format!("{original_name} after each block")); after_list.elements = elements_after; after_list.mapping = mapping_after; + // Each part renders in a fresh render context, which is where handlebars + // registers inline partials, so every part has to define them again. + prepend_inline_partials(&before_list, &mut list_content); + prepend_inline_partials(&before_list, &mut after_list); SplitTemplate { before_list, list_content, @@ -64,6 +68,25 @@ impl AsyncFromStrWithState for SplitTemplate { } } +fn prepend_inline_partials(defining: &Template, using: &mut Template) { + let (elements, mapping): (Vec<_>, Vec<_>) = defining + .elements + .iter() + .zip(&defining.mapping) + .filter(|(element, _)| is_inline_partial(element)) + .map(|(element, position)| (element.clone(), position.clone())) + .unzip(); + using.elements.splice(0..0, elements); + using.mapping.splice(0..0, mapping); +} + +fn is_inline_partial(element: &TemplateElement) -> bool { + use handlebars::template::Parameter::Name; + matches!(element, + TemplateElement::DecoratorBlock(decorator) + if matches!(&decorator.name, Name(name) if name == "inline")) +} + fn is_template_list_item(element: &TemplateElement) -> bool { use Parameter::Name; use handlebars::template::Parameter;