Skip to main content

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}