molt/layout.rs
1//! Reusable page layouts.
2
3use crate::core::Node;
4use crate::elements::div;
5
6/// Wraps content in a consistent structure: a header, a footer, navigation.
7///
8/// # A footgun that does not exist here
9///
10/// A component is a value, not a shared handle. [`Node`] is a value type, so using one
11/// instance in two places copies it and the copies cannot affect each other. Write components
12/// however you like: a function, a struct, a constant.
13///
14/// # Examples
15/// ```
16/// use molt::prelude::*;
17/// use molt::Layout;
18///
19/// struct Blog { site_title: String }
20///
21/// impl Layout for Blog {
22/// fn wrap(&self, content: Node) -> Node {
23/// body()
24/// .child(header().child(h1().text(&self.site_title)))
25/// .child(main_tag().child(content))
26/// .child(footer().child(p().text("© 2026")))
27/// .into()
28/// }
29/// }
30///
31/// let page = Blog { site_title: "RideKeeper".into() }.wrap(p().text("Hello").into());
32/// assert!(page.render().contains("<h1>RideKeeper</h1>"));
33/// ```
34pub trait Layout {
35 /// Wraps a single node in the layout.
36 ///
37 /// Named `wrap`, not `render`: a type that is both a `Layout` and a [`Render`](crate::core::Render)
38 /// would otherwise have two `render` methods and every call would be ambiguous.
39 fn wrap(&self, content: Node) -> Node;
40
41 /// Wraps several nodes, grouping them in a `<div>` first.
42 ///
43 /// Use [`Node::fragment`] and [`Layout::wrap`] if you would rather not have the wrapper.
44 fn wrap_many(&self, contents: Vec<Node>) -> Node {
45 self.wrap(div().children_from(contents).into())
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::core::Render;
53 use crate::elements::{body, h1, main_tag, p};
54
55 struct Minimal;
56
57 impl Layout for Minimal {
58 fn wrap(&self, content: Node) -> Node {
59 body()
60 .child(h1().text("Site"))
61 .child(main_tag().child(content))
62 .into()
63 }
64 }
65
66 #[test]
67 fn a_layout_wraps_a_single_node() {
68 let page = Minimal.wrap(p().text("body").into());
69 assert_eq!(
70 page.render(),
71 "<body><h1>Site</h1><main><p>body</p></main></body>"
72 );
73 }
74
75 #[test]
76 fn wrap_many_groups_the_contents_in_a_div() {
77 let page = Minimal.wrap_many(vec![p().text("a").into(), p().text("b").into()]);
78 assert_eq!(
79 page.render(),
80 "<body><h1>Site</h1><main><div><p>a</p><p>b</p></div></main></body>"
81 );
82 }
83
84 #[test]
85 fn wrap_many_with_no_contents_still_produces_the_wrapper() {
86 let page = Minimal.wrap_many(vec![]);
87 assert!(page.render().contains("<div></div>"));
88 }
89}