feat(build): add concise render helper
Generate render(view) as the short page/fragment composition helper while keeping render_html(view) as an explicit compatibility alias, then move examples to the leaner happy path. req: component/003 req: dx/006 req: view/001
This commit is contained in:
+1
-1
@@ -76,7 +76,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
|
|||||||
005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”.
|
005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”.
|
||||||
|
|
||||||
### req: dx/006
|
### req: dx/006
|
||||||
006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. These return `impl IntoEffect`. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star]
|
006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. Generated view modules also expose `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition; `render_html(view)` remains as an explicit compatibility alias. These return `impl IntoEffect` or `SafeHtml` at the boundary. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star]
|
||||||
|
|
||||||
### req: dx/007
|
### req: dx/007
|
||||||
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
|
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ mod tests {
|
|||||||
// req: html_safety/002 req: view/001 req: test/005
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
#[test]
|
#[test]
|
||||||
fn kanban_board_test_payload_is_rendered_by_a_hemplate_view() {
|
fn kanban_board_test_payload_is_rendered_by_a_hemplate_view() {
|
||||||
let html = super::ui::render_html(&empty_board());
|
let html = super::ui::render(&empty_board());
|
||||||
let document = Html::parse_fragment(html.as_str());
|
let document = Html::parse_fragment(html.as_str());
|
||||||
assert_eq!(document.select(&selector(".columns")).count(), 1);
|
assert_eq!(document.select(&selector(".columns")).count(), 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ fn parse_column(value: Option<&str>) -> usize {
|
|||||||
fn page_html(board: &BoardState) -> String {
|
fn page_html(board: &BoardState) -> String {
|
||||||
// Explicit full-page composition boundary for already-rendered hemplate fragments.
|
// Explicit full-page composition boundary for already-rendered hemplate fragments.
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
board_ui::render_html(&Board {
|
board_ui::render(&Board {
|
||||||
options: render_options(),
|
options: render_options(),
|
||||||
board: render_board(board),
|
board: render_board(board),
|
||||||
})
|
})
|
||||||
@@ -262,7 +262,7 @@ fn shell(body: String) -> String {
|
|||||||
|
|
||||||
fn render_options() -> SafeHtml {
|
fn render_options() -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
ui::render_html(&ColumnOptions {
|
ui::render(&ColumnOptions {
|
||||||
options: COLUMNS
|
options: COLUMNS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, title)| ColumnOption { id, title })
|
.map(|(id, title)| ColumnOption { id, title })
|
||||||
@@ -291,7 +291,7 @@ fn board_view(board: &BoardState) -> BoardColumns {
|
|||||||
|
|
||||||
fn render_board(board: &BoardState) -> SafeHtml {
|
fn render_board(board: &BoardState) -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
ui::render_html(&board_view(board))
|
ui::render(&board_view(board))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_card(card: &Card) -> BoardCard {
|
fn render_card(card: &Card) -> BoardCard {
|
||||||
@@ -354,7 +354,7 @@ mod tests {
|
|||||||
// req: html_safety/002 req: view/001 req: test/005
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
#[test]
|
#[test]
|
||||||
fn presence_payload_is_rendered_by_a_hemplate_view() {
|
fn presence_payload_is_rendered_by_a_hemplate_view() {
|
||||||
let html = ui::render_html(&Presence { count: 7 });
|
let html = ui::render(&Presence { count: 7 });
|
||||||
let document = Html::parse_fragment(html.as_str());
|
let document = Html::parse_fragment(html.as_str());
|
||||||
assert_eq!(document.select(&selector("span.presence")).count(), 2);
|
assert_eq!(document.select(&selector("span.presence")).count(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ fn page_html(demo: &DemoState) -> String {
|
|||||||
fn shell(body: String) -> String {
|
fn shell(body: String) -> String {
|
||||||
// Explicit full-page shell composition boundary for already-rendered hemplate fragments.
|
// Explicit full-page shell composition boundary for already-rendered hemplate fragments.
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::app_shell::render_html(&AppShell { body: SafeHtml::trusted(body) }).into_string()
|
ui::app_shell::render(&AppShell { body: SafeHtml::trusted(body) }).into_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hero_view(demo: &DemoState) -> HeroMetrics {
|
fn hero_view(demo: &DemoState) -> HeroMetrics {
|
||||||
@@ -479,7 +479,7 @@ fn hero_view(demo: &DemoState) -> HeroMetrics {
|
|||||||
|
|
||||||
fn render_hero(demo: &DemoState) -> SafeHtml {
|
fn render_hero(demo: &DemoState) -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&hero_view(demo))
|
ui::render(&hero_view(demo))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_view(demo: &DemoState) -> BoardLanes {
|
fn board_view(demo: &DemoState) -> BoardLanes {
|
||||||
@@ -506,7 +506,7 @@ fn board_view(demo: &DemoState) -> BoardLanes {
|
|||||||
|
|
||||||
fn render_board(demo: &DemoState) -> SafeHtml {
|
fn render_board(demo: &DemoState) -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&board_view(demo))
|
ui::render(&board_view(demo))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
|
fn issue_card(item: &WorkItem, selected: bool) -> IssueCard {
|
||||||
@@ -535,7 +535,7 @@ fn activity_view(demo: &DemoState) -> ActivityFeed {
|
|||||||
|
|
||||||
fn render_activity(demo: &DemoState) -> SafeHtml {
|
fn render_activity(demo: &DemoState) -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&activity_view(demo))
|
ui::render(&activity_view(demo))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
||||||
@@ -544,9 +544,9 @@ fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
|||||||
.selected_id
|
.selected_id
|
||||||
.and_then(|id| demo.work.iter().find(|item| item.id == id));
|
.and_then(|id| demo.work.iter().find(|item| item.id == id));
|
||||||
let selected = selected.map_or_else(
|
let selected = selected.map_or_else(
|
||||||
|| ui::render_html(&InspectorEmpty),
|
|| ui::render(&InspectorEmpty),
|
||||||
|item| {
|
|item| {
|
||||||
ui::render_html(&InspectorSelected {
|
ui::render(&InspectorSelected {
|
||||||
title: item.title.clone(),
|
title: item.title.clone(),
|
||||||
lane: LANES[item.lane].1,
|
lane: LANES[item.lane].1,
|
||||||
stage: item.stage.label(),
|
stage: item.stage.label(),
|
||||||
@@ -562,32 +562,32 @@ fn inspector_view(demo: &DemoState) -> InspectorPanel {
|
|||||||
|
|
||||||
fn render_inspector(demo: &DemoState) -> SafeHtml {
|
fn render_inspector(demo: &DemoState) -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&inspector_view(demo))
|
ui::render(&inspector_view(demo))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn architecture_hero() -> SafeHtml {
|
fn architecture_hero() -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
ui::render_html(&ArchitectureHero)
|
ui::render(&ArchitectureHero)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn architecture_board() -> SafeHtml {
|
fn architecture_board() -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
ui::render_html(&ArchitectureBoard)
|
ui::render(&ArchitectureBoard)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn architecture_inspector() -> SafeHtml {
|
fn architecture_inspector() -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
ui::render_html(&ArchitectureInspector)
|
ui::render(&ArchitectureInspector)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn architecture_activity() -> SafeHtml {
|
fn architecture_activity() -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001
|
// req: html_safety/002 req: view/001
|
||||||
ui::render_html(&ArchitectureActivity)
|
ui::render(&ArchitectureActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_control_center(page: ControlCenter) -> String {
|
fn render_control_center(page: ControlCenter) -> String {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&page).into_string()
|
ui::render(&page).into_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -705,7 +705,7 @@ mod tests {
|
|||||||
// req: html_safety/002 req: view/001 req: test/005
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
#[test]
|
#[test]
|
||||||
fn live_feed_payload_is_rendered_by_a_hemplate_view() {
|
fn live_feed_payload_is_rendered_by_a_hemplate_view() {
|
||||||
let html = ui::render_html(&LiveFeed { tick: 7 });
|
let html = ui::render(&LiveFeed { tick: 7 });
|
||||||
let document = Html::parse_fragment(html.as_str());
|
let document = Html::parse_fragment(html.as_str());
|
||||||
let row = document
|
let row = document
|
||||||
.select(&selector(".live-row"))
|
.select(&selector(".live-row"))
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ fn all_examples() -> String {
|
|||||||
fn shell(body: String) -> String {
|
fn shell(body: String) -> String {
|
||||||
// Explicit full-page shell composition boundary for already-rendered hemplate fragments.
|
// Explicit full-page shell composition boundary for already-rendered hemplate fragments.
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&AppShell {
|
ui::render(&AppShell {
|
||||||
body: SafeHtml::trusted(body),
|
body: SafeHtml::trusted(body),
|
||||||
})
|
})
|
||||||
.into_string()
|
.into_string()
|
||||||
@@ -215,7 +215,7 @@ fn todos_view(todos: &[Todo]) -> TodoItems {
|
|||||||
|
|
||||||
fn render_page_swap(title: &'static str, message: &'static str) -> String {
|
fn render_page_swap(title: &'static str, message: &'static str) -> String {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
page_swap::render_html(&PageSwap {
|
page_swap::render(&PageSwap {
|
||||||
content: render_docs_content(message),
|
content: render_docs_content(message),
|
||||||
title,
|
title,
|
||||||
})
|
})
|
||||||
@@ -224,7 +224,7 @@ fn render_page_swap(title: &'static str, message: &'static str) -> String {
|
|||||||
|
|
||||||
fn render_docs_content(message: &'static str) -> SafeHtml {
|
fn render_docs_content(message: &'static str) -> SafeHtml {
|
||||||
// req: html_safety/002 req: view/001 req: component/003
|
// req: html_safety/002 req: view/001 req: component/003
|
||||||
ui::render_html(&DocsContent { message })
|
ui::render(&DocsContent { message })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -298,7 +298,7 @@ mod tests {
|
|||||||
fn todos_payload_is_rendered_by_a_hemplate_view() {
|
fn todos_payload_is_rendered_by_a_hemplate_view() {
|
||||||
let todos = vec![Todo { id: 7, title: "<b>Ship v0</b>".to_owned() }];
|
let todos = vec![Todo { id: 7, title: "<b>Ship v0</b>".to_owned() }];
|
||||||
|
|
||||||
let html = ui::render_html(&todos_view(&todos));
|
let html = ui::render(&todos_view(&todos));
|
||||||
let document = Html::parse_fragment(html.as_str());
|
let document = Html::parse_fragment(html.as_str());
|
||||||
let rows = document.select(&selector("li[data-key]")).collect::<Vec<_>>();
|
let rows = document.select(&selector("li[data-key]")).collect::<Vec<_>>();
|
||||||
assert_eq!(rows.len(), 1);
|
assert_eq!(rows.len(), 1);
|
||||||
@@ -310,7 +310,7 @@ mod tests {
|
|||||||
// req: html_safety/002 req: view/001 req: test/005
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_todos_payload_is_rendered_by_a_hemplate_view() {
|
fn empty_todos_payload_is_rendered_by_a_hemplate_view() {
|
||||||
let html = ui::render_html(&todos_view(&[]));
|
let html = ui::render(&todos_view(&[]));
|
||||||
let document = Html::parse_fragment(html.as_str());
|
let document = Html::parse_fragment(html.as_str());
|
||||||
let rows = document.select(&selector("li")).collect::<Vec<_>>();
|
let rows = document.select(&selector("li")).collect::<Vec<_>>();
|
||||||
assert_eq!(rows.len(), 1);
|
assert_eq!(rows.len(), 1);
|
||||||
|
|||||||
@@ -524,11 +524,12 @@ impl Resources {
|
|||||||
}
|
}
|
||||||
out.push_str(&format!("__slhx_lower_html(html.as_ref(), &{table_name})\n"));
|
out.push_str(&format!("__slhx_lower_html(html.as_ref(), &{table_name})\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{\n"));
|
out.push_str(&format!("{pad}pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{\n"));
|
||||||
out.push_str(&format!("{pad} let mut html = ::std::string::String::new();\n"));
|
out.push_str(&format!("{pad} let mut html = ::std::string::String::new();\n"));
|
||||||
out.push_str(&format!("{pad} ::hemplate::Hemplate::render_into(view, &mut html).expect(\"slhx hemplate view renders\");\n"));
|
out.push_str(&format!("{pad} ::hemplate::Hemplate::render_into(view, &mut html).expect(\"slhx hemplate view renders\");\n"));
|
||||||
out.push_str(&format!("{pad} ::slhx::SafeHtml::trusted(lower_html(html))\n"));
|
out.push_str(&format!("{pad} ::slhx::SafeHtml::trusted(lower_html(html))\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{ render(view) }}\n\n"));
|
||||||
self.push_lowering_table(out, component, indent, &table_name);
|
self.push_lowering_table(out, component, indent, &table_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1298,6 +1299,7 @@ mod tests {
|
|||||||
assert!(generated.contains("pub mod todo"));
|
assert!(generated.contains("pub mod todo"));
|
||||||
assert!(generated.contains("pub const ALL_IDS"));
|
assert!(generated.contains("pub const ALL_IDS"));
|
||||||
assert!(generated.contains("pub fn lower_html"));
|
assert!(generated.contains("pub fn lower_html"));
|
||||||
|
assert!(generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("data-slhx-slot"));
|
assert!(generated.contains("data-slhx-slot"));
|
||||||
assert!(generated.contains("data-slhx-form"));
|
assert!(generated.contains("data-slhx-form"));
|
||||||
@@ -1364,6 +1366,7 @@ mod tests {
|
|||||||
assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod components"));
|
assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod components"));
|
||||||
assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod slots"));
|
assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod slots"));
|
||||||
assert!(generated.contains("\npub fn lower_html"));
|
assert!(generated.contains("\npub fn lower_html"));
|
||||||
|
assert!(generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
assert!(generated.contains("\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
|
||||||
assert!(generated.contains("pub mod todo"));
|
assert!(generated.contains("pub mod todo"));
|
||||||
assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")"));
|
assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")"));
|
||||||
|
|||||||
Reference in New Issue
Block a user